基于Python+Django的LangGraph智能旅游规划系统

基于Python+Django的LangGraph智能旅游规划系统 毕业论文

毕业设计


一、项目名称:

基于Python+Django的LangGraph智能旅游规划系统


【摘要】

在线旅游平台积累了大量景点、票价与评价信息,但本科课程设计场景下仍常见两类问题:一是行程规划依赖人工查阅,难以按城市、天数、预算与偏好自动生成可解释的日程;二是景点数据与分析图表分散,缺少与规划结果联动的可视化。本文设计并实现基于 Python 与 Django 的 LangGraph 智能旅游规划系统,将景点检索、攻略发布、智能行程生成与多维数据分析放在同一套 B/S 应用中,便于毕业设计本地演示与答辩讲解。

系统采用 Django 3.1 与 SimpleUI,运行端口 8100 ,数据库为 MySQL,库名 travel_plan,通过 Django ORM 访问。全文以本机可运行为准,不依赖云服务器部署说明。前台自建 User 会话认证(middleware/auth.py),后台使用 Django 自带管理员与 SimpleUI 菜单。业务表包括 app_userapp_attractionapp_favoriteapp_commentapp_guideapp_planrecord。景点数据以来源去哪儿网的样本 CSV 导入为主,当前库内约 4900 条景点、覆盖 300 余座城市。智能规划在 app/utils/travel_agent.py 中实现「意图解析 → 本城景点检索 → 日程排程 → 大模型写行程」四节点流水线:优先使用 LangGraph 状态图编排,未安装或调用失败时自动降级为同构顺序流水线;大模型走智谱 OpenAI 兼容接口,默认模型 glm-4-flash,密钥仅写在项目 .env。规划成功后将行程同步为攻略记录,并可用高德 Web 服务补全坐标后在地图上按顺序标注。无高德 Key 时仍保留文字行程与景点列表,保证核心功能可演示。分析页由 travel_charts.py 一次返回多图数据,配合本地缓存与版本失效;前端并行请求筛选项与图表数据包。静态资源统一放在 static/vendor/ 本地目录,前端库不依赖外网 CDN。前台演示账号为 admin/123456,后台为 admin/admin123。摘要只概括技术路线与规模,具体接口与操作步骤见正文第 4、5 章。

测试表明,系统可完成登录注册、景点浏览收藏、攻略阅读、AI 路线规划、数据分析与 CSV 导入等闭环,规划检索锁定所选城市、分析二次刷新可命中缓存,能够支撑课程设计演示。

从教学验收角度看,老师可以按固定账号进入前台,在景点页按城市筛选,在规划页选择数据充足的城市生成行程,再打开攻略详情核对日卡片排版,最后在分析页悬停城市 TOP 与票价分桶。后台可用另一套账号维护数据。论文后续章节按该演示路径组织,不引入本仓库未实现的推荐模型或支付下单。整条路径大约十五分钟可走完,适合课堂抽查。若时间更紧,可只演示登录、规划与分析三页。

【关键词】 Django;LangGraph;智能旅游规划;数据可视化;ECharts

【Abstract】

Online travel platforms accumulate attraction, ticket and rating data, yet course projects still lack an integrated path from retrieval to explainable itineraries and charts. This thesis implements a Django-based intelligent travel planner with LangGraph-style agent workflow, guides, and multi-dimensional visualization for local demos.

The stack is Django 3.1 and SimpleUI on port 8100 , with MySQL database travel_plan accessed via ORM. Frontend users are stored in a custom User table with session middleware; admin uses SimpleUI. Core tables include users, attractions, favorites, comments, guides and plan records. Attractions are imported from a Qunar sample CSV (about 4900 spots, 300+ cities). Planning runs parse-intent, city-scoped search, scheduling and LLM writing in travel_agent.py, using LangGraph when available and a pipeline fallback otherwise. The LLM is Zhipu glm-4-flash via an OpenAI-compatible API; keys stay in .env. Successful plans can be published as guides. Maps use optional Amap geocoding. Analysis bundles are cached with version invalidation. Static assets are local under static/vendor/. Accounts: front admin/123456, admin admin/admin123.

【Keywords】 Django; LangGraph; Travel Planning; Data Visualization; ECharts

(中文关键词与英文 Keywords 对应,便于检索与答辩材料统一用语。)摘要篇幅控制在一页内可读完,细节放到第 3、4 章,避免摘要写成说明书。关键词五个,覆盖框架、编排、业务主题与可视化手段。正文不再重复堆砌关键词,以免稀释问题陈述。开篇即点明闭环,是全文叙述主线。后面各章都围绕检索、生成、落库与可视化展开说明即可。


目 录

[1 绪 论](#1 绪 论)

  • [1.1 课题研究背景](#1.1 课题研究背景)
  • [1.2 课题研究的目的、意义](#1.2 课题研究的目的、意义)
  • [1.3 课题的国内外研究现状和发展动态](#1.3 课题的国内外研究现状和发展动态)
  • [1.4 研究内容](#1.4 研究内容)
  • [1.5 论文结构安排](#1.5 论文结构安排)

[2 关键技术](#2 关键技术)

  • [2.1 Python与Django框架](#2.1 Python与Django框架)
    • [2.1.1 Python语言与项目组织](#2.1.1 Python语言与项目组织)
    • [2.1.2 Django MTV与中间件](#2.1.2 Django MTV与中间件)
  • [2.2 MySQL与Django ORM](#2.2 MySQL与Django ORM)
    • [2.2.1 关系模型与索引](#2.2.1 关系模型与索引)
    • [2.2.2 缓存与失效策略](#2.2.2 缓存与失效策略)
  • [2.3 LangGraph智能体编排](#2.3 LangGraph智能体编排)
  • [2.4 智谱大模型与提示词](#2.4 智谱大模型与提示词)
  • [2.5 ECharts与高德地图](#2.5 ECharts与高德地图)
  • [2.6 技术特色与创新点](#2.6 技术特色与创新点)

[3 系统分析与设计](#3 系统分析与设计)

  • [3.1 系统总体设计](#3.1 系统总体设计)
    • [3.1.1 系统架构设计](#3.1.1 系统架构设计)
    • [3.1.2 系统数据流设计](#3.1.2 系统数据流设计)
    • [3.1.3 系统功能模块设计](#3.1.3 系统功能模块设计)
  • [3.2 系统详细设计](#3.2 系统详细设计)
    • [3.2.1 用户认证模块设计](#3.2.1 用户认证模块设计)
    • [3.2.2 景点浏览与收藏模块设计](#3.2.2 景点浏览与收藏模块设计)
    • [3.2.3 旅游攻略模块设计](#3.2.3 旅游攻略模块设计)
    • [3.2.4 AI路线规划模块设计](#3.2.4 AI路线规划模块设计)
    • [3.2.5 旅游数据分析模块设计](#3.2.5 旅游数据分析模块设计)
    • [3.2.6 数据爬取与后台管理模块设计](#3.2.6 数据爬取与后台管理模块设计)
  • [3.3 数据库设计](#3.3 数据库设计)
    • [3.3.1 数据库关系设计](#3.3.1 数据库关系设计)

[4 系统实现](#4 系统实现)

  • [4.0 系统整体实现流程](#4.0 系统整体实现流程)
  • [4.1 用户认证模块的实现](#4.1 用户认证模块的实现)
  • [4.2 景点浏览与收藏模块的实现](#4.2 景点浏览与收藏模块的实现)
  • [4.3 旅游攻略模块的实现](#4.3 旅游攻略模块的实现)
  • [4.4 AI路线规划模块的实现](#4.4 AI路线规划模块的实现)
  • [4.5 旅游数据分析模块的实现](#4.5 旅游数据分析模块的实现)
  • [4.6 数据爬取与后台管理模块的实现](#4.6 数据爬取与后台管理模块的实现)
  • [4.7 用户界面实现](#4.7 用户界面实现)
  • [4.8 系统集成与部署](#4.8 系统集成与部署)

[5 系统测试](#5 系统测试)

  • [5.1 系统功能测试](#5.1 系统功能测试)
    • [5.1.1 用户认证功能测试用例](#5.1.1 用户认证功能测试用例)
    • [5.1.2 数据管理功能测试用例](#5.1.2 数据管理功能测试用例)
    • [5.1.3 核心业务功能测试用例](#5.1.3 核心业务功能测试用例)
    • [5.1.4 数据分析功能测试用例](#5.1.4 数据分析功能测试用例)
    • [5.1.5 系统集成功能测试用例](#5.1.5 系统集成功能测试用例)
    • [5.1.6 用户界面功能测试用例](#5.1.6 用户界面功能测试用例)
  • [5.2 系统性能测试](#5.2 系统性能测试)
    • [5.2.1 响应时间性能分析](#5.2.1 响应时间性能分析)
    • [5.2.2 并发性能测试](#5.2.2 并发性能测试)
    • [5.2.3 数据库性能测试](#5.2.3 数据库性能测试)
    • [5.2.4 性能优化措施](#5.2.4 性能优化措施)

[6 总结与展望](#6 总结与展望)

  • [6.1 总结](#6.1 总结)
  • [6.2 展望](#6.2 展望)

参考文献

致谢


1 绪 论

1.1 课题研究背景

大众旅游需求从"买门票、看攻略"转向"按预算与偏好生成可执行日程"。携程、去哪儿等平台提供丰富的景点条目,但开源课程设计往往只做列表展示或单一推荐,缺少把检索、排程、文案生成和图表分析串成闭环的系统。学生若直接调用商业大模型而不约束候选景点,容易出现行程城市与所选目的地不一致、地图标到其它省市、文案无法核对数据来源等问题。

本课题选择 Django 而不是再写一套 Flask 大屏,是因为后台管理、ORM 与会话中间件已经能覆盖作业常见需求,SimpleUI 可以把景点与攻略维护做成可演示的管理端。选择 LangGraph 风格而不是单次聊天,是为了把"查库、选点、写文"拆开,答辩时能对着日志讲每一步。选择去哪儿样本而不是手写十条景点,是为了让分析图有足够类目,也让规划在热门城市有候选。这些选择决定了后文的技术栈与模块边界。

本课题面向毕业设计本地演示:数据落在 MySQL,景点来自去哪儿样本导入而非手写演示库;规划必须先在本城景点集合上选点,再调用大模型撰写日程;分析页对票价分桶、城市 TOP、评分分布等进行可视化。这样既保留智能体编排的教学点,又保证老师可以对照库内记录核验结果。

从数据形态看,去哪儿样本把景点名称、城市、类型、门票、评分、简介、封面地址与开放时间等字段一并给出,适合作为关系表的主数据。导入后若只做列表浏览,系统与普通景点网站没有本质差别;真正需要补齐的是"查询结果如何变成日程"。人工做法是打开多个详情页、对照门票与游玩时长、再手写攻略,耗时且难以复现。程序化做法则是把城市、天数、预算、偏好写成结构化意图,在库内选出有限候选,再交给大模型组织语言。后者必须解决两个工程问题:候选集合必须与所选城市一致;生成文本必须能回写到 PlanRecordGuide,否则首页攻略计数永远为 0,答辩时无法展示闭环。

从使用角色看,前台用户完成浏览、收藏、评论、发布攻略与发起规划;管理员在 SimpleUI 中维护景点与攻略,必要时启动 CSV 导入或去哪儿爬取任务。两套账号刻意分开:前台 app_user 用会话字段 username,后台用 Django 自带管理员。中间件只拦截前台业务路径,/admin/ 交给 Django 自己的登录。端口固定 8100,避免与同机其它作业冲突。静态资源要求本地化,是为了教室断网或 CDN 被墙时界面仍能打开;地图脚本属于业务接口,单独配置 Key,缺 Key 时规划页仍展示行程文字与景点列表。

1.2 课题研究的目的、意义

1.2.1 研究目的

本课题要解决三个具体问题。第一,把登录、景点、收藏评论、攻略、规划、分析、导入和后台管理做成可运行的 Django 应用,端口固定为 8100。第二,实现可降级的规划 Agent:LangGraph 可用则走状态图,否则走同构流水线,保证答辩机未装 langgraph 时仍能演示。第三,规划结果写入 PlanRecord,成功后同步 Guide,首页攻略计数与列表有真实记录。

附带目标是界面观感与交互一致:静态资源可离线加载、分析图系列名称正确、TOP 排序合理、规划页能保持所选城市。目的表述对应可演示路径,避免写成无法落地的空泛概念。

1.2.2 研究意义

理论上,课题把"工具调用式检索 + 大模型生成"落到可复查的数据库记录上,避免纯聊天式行程无法追溯。实践上,系统可在 Windows 本机用 MySQL 与本地静态资源离线演示(地图 JS 除外)。对社会与教学而言,学生可以理解智能体节点划分、缓存失效与可视化验收,而不是只展示一段无法复现的提示词。

若把大模型当成唯一数据源,行程里的景点可能不在库中,票价与评分也无法核对。本系统坚持"库内候选优先、模型只负责叙述":node_search_attractions 先按城市过滤,偏好关键词只在本城结果上加权,候选不足时回退本城高分列表,绝不按名称搜全国。排程节点再按评分、门票与预算切分每日 2 到 3 个点。写行程节点把景点清单嵌入提示词,要求按日输出;接口失败则用规则模板生成带门票与游玩时长的条目。这样一来,即使免费模型限流,页面仍有可讲解的日程骨架。分析模块则回答另一类问题:全库免费占比、城市景点数分布、门票分桶是否被免费票主导等,用图表把导入数据的统计特征讲清楚,而不是只报一条 COUNT。

1.3 课题的国内外研究现状和发展动态

1.3.1 国外研究现状

国外在线旅行社与地图服务长期提供目的地检索与路线绘制。近年大语言模型使"自然语言行程"成为产品方向,但工业系统通常把检索、库存与生成拆成独立服务。LangGraph 一类图编排库把多步骤 Agent 的状态显式化,适合教学中讲解节点与边。开源社区也有基于检索增强的旅行问答示例,但多数不绑定本科常用的 Django+MySQL 作业形态。

从产品形态看,国外平台更强调库存、价格日历与地图导航,大模型多作为客服或行程草稿,最终仍要落到可预订的资源。教学系统做不到库存对接,但可以模仿"先检索后生成"的顺序,让学生理解为什么不能把城市名直接丢给模型。LangGraph 的价值在于把节点写成可单测的函数,状态在节点间传递,日志可以展示给页面。若环境装不上该库,同构流水线保证节点函数仍然被调用,论文中的流程图与代码一一对应。地图方面,高德在国内演示更常见,本系统只在缺坐标时编码,不把路径规划做成独立交通引擎,以免范围失控。

1.3.2 国内研究现状

国内旅游网站强调价格与评价展示,高校课题常见 Django 景点推荐或 Flask 可视化大屏。智能规划类作业容易直接把城市名交给大模型,缺少"先查库再生成"的约束。本系统在检索节点强制按城市过滤,并在页面回填所选城市,避免下拉框排序造成的观感错位。数据分析部分参考国内可视化毕设的 ECharts 验收习惯:系列写中文名、TOP 横条最大值在上、票价按分位数分桶。

早期演示中曾出现过一种错觉:用户明明规划的是上海,右侧地图却像另一个城市。排查后发现并非检索串库,而是规划表单提交后未回填所选城市,下拉框重新渲染时落在按拼音排在前面的城市(例如七台河),而七台河库内仅有极少景点。于是把"表单回填、地图标题带城市、检索禁止全国回退"三件事一并落实到程序中。论文后续章节按这一真实问题来写设计与测试,而不是虚构一个从未发生的推荐算法。图表方面同样强调可读性:提示框应显示中文系列名,TOP 图数值大的排在上方,门票免费票单独统计。这些约束保证老师悬停图表时看到的名称与数值一致。

1.3.3 发展趋势

行程助手将继续与检索、地图和结构化数据结合。对本课题而言,可扩展方向包括更细的交通接驳、多日住宿约束以及规划失败时的可解释提示,但当前实现边界以课程演示为限。可视化将继续强调可核对:图表系列名、TOP 排序与分桶说明都要能对着数据讲。智能体编排会从单次请求走向带检查点的长任务,但本科作业仍以一次 POST 完成规划为宜。数据来源上,公开样本导入比现场爬取更稳定,爬取模块保留为演示入口,失败时仍可用 CSV。

1.4 研究内容

研究内容包括:Django 会话认证与 SimpleUI 后台;景点筛选、收藏与评论;攻略 Markdown 渲染与 AI 同步;LangGraph/流水线规划与高德补点;分析 bundle 缓存与多图展示;去哪儿爬虫任务与 CSV 导入。技术路线为 MTV 分层、ORM 建模、LocMem 版本缓存、智谱 HTTP 调用。相对列表型旅游作业,创新点在于规划锁定本城、规划写攻略、分析与导入共用失效函数。

具体工作可归纳为四条。其一,完成可运行的 B/S 系统,固定端口与账号,数据库名与论文一致。其二,把规划拆成四个可讲解节点,并实现降级。其三,把分析做成一次 bundle 多图,配合缓存失效,保证导入后数字会变。其四,界面按门户风收敛主色,图标字体本地化,规划页回填城市。论文各章按"问题---技术---设计---实现---测试"展开,代码片段均摘自现仓库,不抄其它课题的高温预警或校园一卡通表述。

1.5 论文结构安排

第 1 章说明背景与内容;第 2 章介绍 Django、MySQL、LangGraph、智谱与 ECharts;第 3 章给出架构、数据流、六模块设计与 ER;第 4 章按模块给出实现流程、代码与界面;第 5 章给出测试用例与性能说明;第 6 章总结量化结果并展望。第 2 章侧重"为什么用这些技术",第 3 章侧重"模块如何划分与数据如何流动",第 4 章给出路径与代码,第 5 章用表格固定操作步骤,避免答辩时步骤与界面不一致。插图集中在第 3、4 章,包括系统架构、数据流、功能结构、各模块流程图以及主要界面截图。参考文献与致谢放在正文之后。


2 关键技术

2.1 Python与Django框架

本项目使用 Python 3.11 与 Django 3.1。工程入口为 manage.py,配置模块为 基于Django系统.settings,应用代码集中在 app/。依赖见 requirements.txt:Django、django-simpleui、PyMySQL、python-dotenv、requests,以及可选的 langgraph 与 langchain-core。选择 3.1 是为了与课程模板及 SimpleUI 兼容组合稳定;Python 3.11 提供更好的错误提示与性能。开发机为 Windows,路径含中文时注意控制台编码。配置中数据库、缓存、中间件与静态目录一次写清,避免答辩机临时改 settings 出错。

2.1.1 Python语言与项目组织

视图、模型、管理命令与工具函数均用 Python 编写。爬取与导入通过 manage.py crawl_qunarimport_qunar_csv 执行,避免把长任务堵在请求线程中。环境变量由 python-dotenv.env 加载,智谱与高德密钥不入库、不进论文正文。工具函数按文件拆分:travel_core.py 负责筛选、缓存失效与攻略同步;travel_agent.py 负责规划节点;travel_charts.py 负责分析聚合;travel_llm.py 负责 HTTP 调用;guide_render.py 负责正文展示;amap_geo.py 负责补点。这种拆分让论文引用路径稳定,修改一处功能时不必重写整包。

2.1.2 Django MTV与中间件

模板位于 templates/app/templates/。根路由将 / 重定向到 /app/login/,业务路由挂在 /app/,后台为 /admin/AuthMiddleware 对未登录访问除登录、注册和 admin 以外的路径一律跳转登录页。前台用户与 Django auth.User 分离,避免作业中混用两套账号体系。

中间件实现见 middleware/auth.py:豁免路径为 /app/login//app/register//admin/,其余请求若 session 中没有 username 则重定向登录。登录成功后首页、景点、攻略、规划、分析、爬取、个人中心均可访问。静态文件由 Django 在 DEBUG 模式下直接提供,travel-theme.css 与分析脚本带版本查询参数,避免浏览器缓存旧主题。模板基类提供侧栏菜单,图标使用本地 Bootstrap Icons 字体文件;若缺少字体文件,侧栏图标可能显示异常,因此字体文件需随项目一并提供。SimpleUI 只改后台外观与菜单分组,不替代前台主题。

2.2 MySQL与Django ORM

数据库引擎为 django.db.backends.mysql,默认库名 travel_plan,字符集 utf8mb4。模型定义在 app/models.py,迁移由 Django 生成。连接可用 PyMySQL 作为 MySQLdb 替代,便于在 Windows 上安装。库名与论文、导出 SQL 文件名保持一致,便于环境还原。表结构变更通过 Django 迁移文件管理。演示数据以导入 CSV 为准,论文中的景点数与城市数以实际库统计为准。

2.2.1 关系模型与索引

Attractioncitycategoryis_free 建索引,并有 (city, name) 联合索引,支撑列表筛选与规划检索。Favorite(user, attraction) 唯一约束防止重复收藏。Guide 与景点为多对多,便于攻略详情展示关联卡片。规划表用 JSON 文本保存景点顺序,省去额外行程明细表,适合课程规模。评论表按时间倒序,详情页直接切片展示。用户表与 Django 管理员表分离后,前台改密不会误改后台口令。字符集 utf8mb4 保证景点名称中的生僻字可以入库。

2.2.2 缓存与失效策略

CACHES 使用 LocMem,TRAVEL_CACHE_TTL 默认 120 秒。travel_core.invalidate_travel_cache 递增版本号;导入、后台保存、规划写入后调用,使首页统计与分析 bundle 失效。分析查询 filtered_attractions 使用 order_by() 空排序,避免全表按时间排序拖慢聚合。

分析接口把筛选项与图表数据拆成两个地址。筛选项返回中带版本号,与缓存版本对应。前端可先展示本地暂存结果以缩短白屏,再请求最新数据;版本不一致时用新结果覆盖,避免导入数据后页面仍显示旧指标。本地内存缓存随进程存在,重启服务后自然清空,适合单机演示。写入路径统一调用失效函数,避免只改景点表却忘记刷新首页统计。

2.3 LangGraph智能体编排

travel_agent.py 尝试导入 langgraph.graph.StateGraph。成功则编译四节点图:parse_intentsearch_attractionsrank_and_schedulewrite_itinerary。失败或 invoke 未带回 spots 时改走 _run_pipeline,日志中记录引擎名 langgraphpipeline,便于界面展示与论文说明。

状态类型为 PlanState,字段包括城市、预算、天数、偏好、候选列表、入选景点、行程正文与日志。parse_intent 把天数限制在 1 至 7,城市缺省时才使用北京,避免空字符串进入查询。rank_and_schedule 在预算内优先高分景点,并按天数切块。write_itinerary 调用 chat_completion;返回值以 [ERROR] 开头则改用 _fallback_itineraryrun_travel_plan 对外只暴露一个函数,视图不必关心当前引擎。进程内缓存编译后的图对象,避免每次请求重新 compile

2.4 智谱大模型与提示词

travel_llm.py{ZHIPU_BASE_URL}/chat/completions 发送 Bearer 请求,默认模型 glm-4-flash。无密钥、超时或 HTTP 错误时返回以 [ERROR] 开头的字符串,上层改用规则模板生成日程,保证断网或限流时页面仍有行程骨架。

提示词由 build_itinerary_prompt 组装:系统角色要求按中文分日输出,用户侧给出城市、天数、预算、偏好以及候选景点的名称、门票、评分与建议时长。不把密钥写进仓库与论文。免费档有并发与每分钟次数限制,规划页按"提交一次、等待结果"设计,不在前端轮询多次生成。超时时间在工具函数中设置,避免请求线程长时间占用。模型输出仍可能夹杂 Markdown 标记,因此攻略详情与规划详情共用 guide_render 做轻量转换,把标题与列表变成 HTML 卡片。

2.5 ECharts与高德地图

分析页与首页城市分布使用本地 ECharts 脚本。辅助函数提供横向排行图配置,数值较大的项显示在上方,系列使用中文名称,便于阅读提示信息。地图页按需加载高德脚本;缺坐标景点可通过地理编码补全并写回表。

坐标补全策略是:仅当纬度为 0 且经度为 0 时才调用地理编码,避免覆盖已有正确坐标,也减少配额消耗。补全结果可写回 Attraction,下次规划同一城市时少一次外网请求。分析页图表种类包括城市 TOP 柱、类型玫瑰、票价面积线、评分分布柱、游玩时长柱、免费占比漏斗、评分与票价散点、高分 TOP、收藏 TOP、城市雷达、规划次数柱与规划天数饼。散点按步长抽样,控制在约 400 个点,防止一次渲染数千点导致卡顿。页面模板改为引用本地静态文件,降低外网不可用时的展示风险。

2.6 技术特色与创新点

算法方面,规划检索按目标城市过滤,避免行程景点落到其它城市。数据处理方面,门票按样本分位数分档,免费单独统计。可视化方面,分析页一次返回多种图表。架构方面,LangGraph 与同构流水线可切换,依赖缺失时仍能演示。体验方面,规划成功可同步攻略,详情页对日程正文做排版。工程方面,缓存版本把写入与统计读取联系起来。界面方面,主色克制,价格色仅用于票价。上述特点均可在源码与运行界面中核对。

与仅做景点列表的系统相比,本系统把检索、排程、文案生成与攻略落库串成可复查流程;与只调用一次聊天接口的做法相比,页面可展示节点日志与候选数量;与图表写死静态数据的做法相比,本系统从数据库聚合并支持缓存刷新。这些差异构成本文技术特色,也是测试设计的依据。


3 系统分析与设计

3.1 系统总体设计

3.1.1 系统架构设计

系统为典型三层 B/S 结构。浏览器加载 Bootstrap、本地图标字体与主题 CSS;Django 视图处理会话与业务;MySQL 持久化;智谱与高德为可选外部服务。表现层不直接访问数据库,所有筛选与规划都经过视图与工具函数。业务层把认证、景点、规划、缓存、导入拆开,避免一个巨型视图文件难以维护。数据访问层以 ORM 为主,大模型与地图走 HTTP。这种划分与第 2 章技术选型一致,也方便第 4 章按模块贴代码。

图3.1 系统架构图
#mermaid-svg-NxjGhHLmOdap5n93{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NxjGhHLmOdap5n93 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NxjGhHLmOdap5n93 .error-icon{fill:#552222;}#mermaid-svg-NxjGhHLmOdap5n93 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NxjGhHLmOdap5n93 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NxjGhHLmOdap5n93 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NxjGhHLmOdap5n93 .marker.cross{stroke:#333333;}#mermaid-svg-NxjGhHLmOdap5n93 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NxjGhHLmOdap5n93 p{margin:0;}#mermaid-svg-NxjGhHLmOdap5n93 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NxjGhHLmOdap5n93 .cluster-label text{fill:#333;}#mermaid-svg-NxjGhHLmOdap5n93 .cluster-label span{color:#333;}#mermaid-svg-NxjGhHLmOdap5n93 .cluster-label span p{background-color:transparent;}#mermaid-svg-NxjGhHLmOdap5n93 .label text,#mermaid-svg-NxjGhHLmOdap5n93 span{fill:#333;color:#333;}#mermaid-svg-NxjGhHLmOdap5n93 .node rect,#mermaid-svg-NxjGhHLmOdap5n93 .node circle,#mermaid-svg-NxjGhHLmOdap5n93 .node ellipse,#mermaid-svg-NxjGhHLmOdap5n93 .node polygon,#mermaid-svg-NxjGhHLmOdap5n93 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NxjGhHLmOdap5n93 .rough-node .label text,#mermaid-svg-NxjGhHLmOdap5n93 .node .label text,#mermaid-svg-NxjGhHLmOdap5n93 .image-shape .label,#mermaid-svg-NxjGhHLmOdap5n93 .icon-shape .label{text-anchor:middle;}#mermaid-svg-NxjGhHLmOdap5n93 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NxjGhHLmOdap5n93 .rough-node .label,#mermaid-svg-NxjGhHLmOdap5n93 .node .label,#mermaid-svg-NxjGhHLmOdap5n93 .image-shape .label,#mermaid-svg-NxjGhHLmOdap5n93 .icon-shape .label{text-align:center;}#mermaid-svg-NxjGhHLmOdap5n93 .node.clickable{cursor:pointer;}#mermaid-svg-NxjGhHLmOdap5n93 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NxjGhHLmOdap5n93 .arrowheadPath{fill:#333333;}#mermaid-svg-NxjGhHLmOdap5n93 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NxjGhHLmOdap5n93 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NxjGhHLmOdap5n93 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NxjGhHLmOdap5n93 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NxjGhHLmOdap5n93 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NxjGhHLmOdap5n93 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NxjGhHLmOdap5n93 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NxjGhHLmOdap5n93 .cluster text{fill:#333;}#mermaid-svg-NxjGhHLmOdap5n93 .cluster span{color:#333;}#mermaid-svg-NxjGhHLmOdap5n93 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NxjGhHLmOdap5n93 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NxjGhHLmOdap5n93 rect.text{fill:none;stroke-width:0;}#mermaid-svg-NxjGhHLmOdap5n93 .icon-shape,#mermaid-svg-NxjGhHLmOdap5n93 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NxjGhHLmOdap5n93 .icon-shape p,#mermaid-svg-NxjGhHLmOdap5n93 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NxjGhHLmOdap5n93 .icon-shape .label rect,#mermaid-svg-NxjGhHLmOdap5n93 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NxjGhHLmOdap5n93 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NxjGhHLmOdap5n93 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NxjGhHLmOdap5n93 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 数据访问层
业务逻辑层
表现层
登录注册
首页与景点
攻略与规划
数据分析
SimpleUI后台
Session认证
景点筛选收藏
LangGraph规划Agent
分析聚合缓存
爬取导入任务
Django ORM
MySQL travel_plan
智谱API
高德地理编码

3.1.2 系统数据流设计

用户提交登录、筛选、规划与导入请求;系统读写景点与规划表,必要时请求大模型生成文案,再返回页面、图表 JSON 与地图坐标。顶层图强调外部实体是用户、MySQL 与智谱;0 层图把六个加工与六张主表对应起来。规划加工同时写规划表与攻略表,分析加工以读景点与规划为主。认证加工只接触用户表。这种对应关系在实现时不得绕过,例如不能把规划结果只留在内存而不落库。

图3.2 顶层数据流图
#mermaid-svg-niGiHqQ6lLKEQg9Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-niGiHqQ6lLKEQg9Y .error-icon{fill:#552222;}#mermaid-svg-niGiHqQ6lLKEQg9Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-niGiHqQ6lLKEQg9Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .marker.cross{stroke:#333333;}#mermaid-svg-niGiHqQ6lLKEQg9Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-niGiHqQ6lLKEQg9Y p{margin:0;}#mermaid-svg-niGiHqQ6lLKEQg9Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .cluster-label text{fill:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .cluster-label span{color:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .cluster-label span p{background-color:transparent;}#mermaid-svg-niGiHqQ6lLKEQg9Y .label text,#mermaid-svg-niGiHqQ6lLKEQg9Y span{fill:#333;color:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .node rect,#mermaid-svg-niGiHqQ6lLKEQg9Y .node circle,#mermaid-svg-niGiHqQ6lLKEQg9Y .node ellipse,#mermaid-svg-niGiHqQ6lLKEQg9Y .node polygon,#mermaid-svg-niGiHqQ6lLKEQg9Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .rough-node .label text,#mermaid-svg-niGiHqQ6lLKEQg9Y .node .label text,#mermaid-svg-niGiHqQ6lLKEQg9Y .image-shape .label,#mermaid-svg-niGiHqQ6lLKEQg9Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-niGiHqQ6lLKEQg9Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .rough-node .label,#mermaid-svg-niGiHqQ6lLKEQg9Y .node .label,#mermaid-svg-niGiHqQ6lLKEQg9Y .image-shape .label,#mermaid-svg-niGiHqQ6lLKEQg9Y .icon-shape .label{text-align:center;}#mermaid-svg-niGiHqQ6lLKEQg9Y .node.clickable{cursor:pointer;}#mermaid-svg-niGiHqQ6lLKEQg9Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .arrowheadPath{fill:#333333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-niGiHqQ6lLKEQg9Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-niGiHqQ6lLKEQg9Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-niGiHqQ6lLKEQg9Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-niGiHqQ6lLKEQg9Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .cluster text{fill:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y .cluster span{color:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-niGiHqQ6lLKEQg9Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-niGiHqQ6lLKEQg9Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-niGiHqQ6lLKEQg9Y .icon-shape,#mermaid-svg-niGiHqQ6lLKEQg9Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-niGiHqQ6lLKEQg9Y .icon-shape p,#mermaid-svg-niGiHqQ6lLKEQg9Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-niGiHqQ6lLKEQg9Y .icon-shape .label rect,#mermaid-svg-niGiHqQ6lLKEQg9Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-niGiHqQ6lLKEQg9Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-niGiHqQ6lLKEQg9Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-niGiHqQ6lLKEQg9Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 登录浏览规划分析
页面与ECharts
行程与地图
景点攻略规划
读写请求
提示词
行程文案
用户
智能旅游规划系统
MySQL
智谱大模型

图3.3 0层数据流图
#mermaid-svg-8unf72UCE2m2hg3D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8unf72UCE2m2hg3D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8unf72UCE2m2hg3D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8unf72UCE2m2hg3D .error-icon{fill:#552222;}#mermaid-svg-8unf72UCE2m2hg3D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8unf72UCE2m2hg3D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8unf72UCE2m2hg3D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8unf72UCE2m2hg3D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8unf72UCE2m2hg3D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8unf72UCE2m2hg3D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8unf72UCE2m2hg3D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8unf72UCE2m2hg3D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8unf72UCE2m2hg3D .marker.cross{stroke:#333333;}#mermaid-svg-8unf72UCE2m2hg3D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8unf72UCE2m2hg3D p{margin:0;}#mermaid-svg-8unf72UCE2m2hg3D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-8unf72UCE2m2hg3D .cluster-label text{fill:#333;}#mermaid-svg-8unf72UCE2m2hg3D .cluster-label span{color:#333;}#mermaid-svg-8unf72UCE2m2hg3D .cluster-label span p{background-color:transparent;}#mermaid-svg-8unf72UCE2m2hg3D .label text,#mermaid-svg-8unf72UCE2m2hg3D span{fill:#333;color:#333;}#mermaid-svg-8unf72UCE2m2hg3D .node rect,#mermaid-svg-8unf72UCE2m2hg3D .node circle,#mermaid-svg-8unf72UCE2m2hg3D .node ellipse,#mermaid-svg-8unf72UCE2m2hg3D .node polygon,#mermaid-svg-8unf72UCE2m2hg3D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-8unf72UCE2m2hg3D .rough-node .label text,#mermaid-svg-8unf72UCE2m2hg3D .node .label text,#mermaid-svg-8unf72UCE2m2hg3D .image-shape .label,#mermaid-svg-8unf72UCE2m2hg3D .icon-shape .label{text-anchor:middle;}#mermaid-svg-8unf72UCE2m2hg3D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-8unf72UCE2m2hg3D .rough-node .label,#mermaid-svg-8unf72UCE2m2hg3D .node .label,#mermaid-svg-8unf72UCE2m2hg3D .image-shape .label,#mermaid-svg-8unf72UCE2m2hg3D .icon-shape .label{text-align:center;}#mermaid-svg-8unf72UCE2m2hg3D .node.clickable{cursor:pointer;}#mermaid-svg-8unf72UCE2m2hg3D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-8unf72UCE2m2hg3D .arrowheadPath{fill:#333333;}#mermaid-svg-8unf72UCE2m2hg3D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-8unf72UCE2m2hg3D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-8unf72UCE2m2hg3D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8unf72UCE2m2hg3D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-8unf72UCE2m2hg3D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8unf72UCE2m2hg3D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-8unf72UCE2m2hg3D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-8unf72UCE2m2hg3D .cluster text{fill:#333;}#mermaid-svg-8unf72UCE2m2hg3D .cluster span{color:#333;}#mermaid-svg-8unf72UCE2m2hg3D div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-8unf72UCE2m2hg3D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-8unf72UCE2m2hg3D rect.text{fill:none;stroke-width:0;}#mermaid-svg-8unf72UCE2m2hg3D .icon-shape,#mermaid-svg-8unf72UCE2m2hg3D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-8unf72UCE2m2hg3D .icon-shape p,#mermaid-svg-8unf72UCE2m2hg3D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-8unf72UCE2m2hg3D .icon-shape .label rect,#mermaid-svg-8unf72UCE2m2hg3D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-8unf72UCE2m2hg3D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-8unf72UCE2m2hg3D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-8unf72UCE2m2hg3D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户
1.0 用户认证
2.0 景点浏览收藏
3.0 旅游攻略
4.0 AI路线规划
5.0 数据分析
6.0 爬取与后台
app_user
app_attraction
app_favorite
app_comment
app_guide
app_planrecord

3.1.3 系统功能模块设计

结合侧栏菜单,功能划分为六大模块:用户认证;景点浏览与收藏;旅游攻略;AI 路线规划;旅游数据分析;数据爬取与后台管理。个人中心改密并入认证模块叙述。模块划分与侧栏一一对应,答辩时可以边点边讲。AI 规划与攻略虽有数据联动,仍分成两个模块,是因为入口、权限与界面职责不同:规划强调表单与地图,攻略强调阅读与发布。分析模块只读聚合,不写行程。爬取与后台面向数据维护,与前台浏览分离,降低误操作风险。

图3.4 系统功能结构图
#mermaid-svg-qpmsEMpUiQR7w7tv{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qpmsEMpUiQR7w7tv .error-icon{fill:#552222;}#mermaid-svg-qpmsEMpUiQR7w7tv .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qpmsEMpUiQR7w7tv .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qpmsEMpUiQR7w7tv .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qpmsEMpUiQR7w7tv .marker.cross{stroke:#333333;}#mermaid-svg-qpmsEMpUiQR7w7tv svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qpmsEMpUiQR7w7tv p{margin:0;}#mermaid-svg-qpmsEMpUiQR7w7tv .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv .cluster-label text{fill:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv .cluster-label span{color:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv .cluster-label span p{background-color:transparent;}#mermaid-svg-qpmsEMpUiQR7w7tv .label text,#mermaid-svg-qpmsEMpUiQR7w7tv span{fill:#333;color:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv .node rect,#mermaid-svg-qpmsEMpUiQR7w7tv .node circle,#mermaid-svg-qpmsEMpUiQR7w7tv .node ellipse,#mermaid-svg-qpmsEMpUiQR7w7tv .node polygon,#mermaid-svg-qpmsEMpUiQR7w7tv .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qpmsEMpUiQR7w7tv .rough-node .label text,#mermaid-svg-qpmsEMpUiQR7w7tv .node .label text,#mermaid-svg-qpmsEMpUiQR7w7tv .image-shape .label,#mermaid-svg-qpmsEMpUiQR7w7tv .icon-shape .label{text-anchor:middle;}#mermaid-svg-qpmsEMpUiQR7w7tv .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qpmsEMpUiQR7w7tv .rough-node .label,#mermaid-svg-qpmsEMpUiQR7w7tv .node .label,#mermaid-svg-qpmsEMpUiQR7w7tv .image-shape .label,#mermaid-svg-qpmsEMpUiQR7w7tv .icon-shape .label{text-align:center;}#mermaid-svg-qpmsEMpUiQR7w7tv .node.clickable{cursor:pointer;}#mermaid-svg-qpmsEMpUiQR7w7tv .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qpmsEMpUiQR7w7tv .arrowheadPath{fill:#333333;}#mermaid-svg-qpmsEMpUiQR7w7tv .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qpmsEMpUiQR7w7tv .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qpmsEMpUiQR7w7tv .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qpmsEMpUiQR7w7tv .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qpmsEMpUiQR7w7tv .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qpmsEMpUiQR7w7tv .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qpmsEMpUiQR7w7tv .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qpmsEMpUiQR7w7tv .cluster text{fill:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv .cluster span{color:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qpmsEMpUiQR7w7tv .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qpmsEMpUiQR7w7tv rect.text{fill:none;stroke-width:0;}#mermaid-svg-qpmsEMpUiQR7w7tv .icon-shape,#mermaid-svg-qpmsEMpUiQR7w7tv .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qpmsEMpUiQR7w7tv .icon-shape p,#mermaid-svg-qpmsEMpUiQR7w7tv .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qpmsEMpUiQR7w7tv .icon-shape .label rect,#mermaid-svg-qpmsEMpUiQR7w7tv .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qpmsEMpUiQR7w7tv .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qpmsEMpUiQR7w7tv .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qpmsEMpUiQR7w7tv :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 智能旅游规划系统
用户认证
景点服务
旅游攻略
AI路线规划
数据分析
数据与后台
登录注册
个人中心改密
景点浏览筛选
景点详情评论
我的收藏
攻略列表
发布与详情
LangGraph流水线
智谱行程生成
地图标注
筛选与缓存
多维ECharts
景点爬取导入
SimpleUI管理

功能结构图与侧栏菜单保持一致,便于对照讲解。六个一级模块下再拆二级,图面更清晰。个人中心不单独占一级,避免结构图过宽。AI 路线规划下的三个二级节点对应检索、生成与地图展示,数据分析下的两个二级节点对应缓存与图表,后台侧强调导入与数据维护。

3.2 系统详细设计

3.2.1 用户认证模块设计

登录校验 app.User 的用户名与明文密码字段(课程演示约定),成功则 request.session['username']。注册校验两次密码与重名。中间件保证业务页必须登录。后台管理员与前台用户分离。个人中心展示当前用户资料,改密要求原密码正确且两次新密码一致。会话随浏览器关闭策略由 Django 默认配置决定,演示时不要同时开两个前台账号抢同一浏览器会话。登录页不经过中间件拦截,避免重定向死循环。

图3.5 用户认证模块流程图
#mermaid-svg-ceMDbo7H2olsIqJA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ceMDbo7H2olsIqJA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ceMDbo7H2olsIqJA .error-icon{fill:#552222;}#mermaid-svg-ceMDbo7H2olsIqJA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ceMDbo7H2olsIqJA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ceMDbo7H2olsIqJA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ceMDbo7H2olsIqJA .marker.cross{stroke:#333333;}#mermaid-svg-ceMDbo7H2olsIqJA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ceMDbo7H2olsIqJA p{margin:0;}#mermaid-svg-ceMDbo7H2olsIqJA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ceMDbo7H2olsIqJA .cluster-label text{fill:#333;}#mermaid-svg-ceMDbo7H2olsIqJA .cluster-label span{color:#333;}#mermaid-svg-ceMDbo7H2olsIqJA .cluster-label span p{background-color:transparent;}#mermaid-svg-ceMDbo7H2olsIqJA .label text,#mermaid-svg-ceMDbo7H2olsIqJA span{fill:#333;color:#333;}#mermaid-svg-ceMDbo7H2olsIqJA .node rect,#mermaid-svg-ceMDbo7H2olsIqJA .node circle,#mermaid-svg-ceMDbo7H2olsIqJA .node ellipse,#mermaid-svg-ceMDbo7H2olsIqJA .node polygon,#mermaid-svg-ceMDbo7H2olsIqJA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ceMDbo7H2olsIqJA .rough-node .label text,#mermaid-svg-ceMDbo7H2olsIqJA .node .label text,#mermaid-svg-ceMDbo7H2olsIqJA .image-shape .label,#mermaid-svg-ceMDbo7H2olsIqJA .icon-shape .label{text-anchor:middle;}#mermaid-svg-ceMDbo7H2olsIqJA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ceMDbo7H2olsIqJA .rough-node .label,#mermaid-svg-ceMDbo7H2olsIqJA .node .label,#mermaid-svg-ceMDbo7H2olsIqJA .image-shape .label,#mermaid-svg-ceMDbo7H2olsIqJA .icon-shape .label{text-align:center;}#mermaid-svg-ceMDbo7H2olsIqJA .node.clickable{cursor:pointer;}#mermaid-svg-ceMDbo7H2olsIqJA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ceMDbo7H2olsIqJA .arrowheadPath{fill:#333333;}#mermaid-svg-ceMDbo7H2olsIqJA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ceMDbo7H2olsIqJA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ceMDbo7H2olsIqJA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ceMDbo7H2olsIqJA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ceMDbo7H2olsIqJA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ceMDbo7H2olsIqJA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ceMDbo7H2olsIqJA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ceMDbo7H2olsIqJA .cluster text{fill:#333;}#mermaid-svg-ceMDbo7H2olsIqJA .cluster span{color:#333;}#mermaid-svg-ceMDbo7H2olsIqJA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ceMDbo7H2olsIqJA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ceMDbo7H2olsIqJA rect.text{fill:none;stroke-width:0;}#mermaid-svg-ceMDbo7H2olsIqJA .icon-shape,#mermaid-svg-ceMDbo7H2olsIqJA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ceMDbo7H2olsIqJA .icon-shape p,#mermaid-svg-ceMDbo7H2olsIqJA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ceMDbo7H2olsIqJA .icon-shape .label rect,#mermaid-svg-ceMDbo7H2olsIqJA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ceMDbo7H2olsIqJA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ceMDbo7H2olsIqJA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ceMDbo7H2olsIqJA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
打开登录页
校验账号密码
写入Session
跳转首页
结束

3.2.2 景点浏览与收藏模块设计

列表支持城市、类型、是否免费与关键词。详情展示封面、票价、评分、评论与可选地图。收藏表唯一约束用户与景点。城市下拉来自库内去重后的 city 字段,避免写死城市名单与导入数据脱节。免费筛选对应 is_free 或门票为零的展示约定,列表卡片同时给出价格文字颜色(橙色仅用于票价)。收藏失败时提示已收藏,不插入重复行。

图3.6 景点浏览与收藏模块流程图
#mermaid-svg-XtfpNySjtmUxHaL6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XtfpNySjtmUxHaL6 .error-icon{fill:#552222;}#mermaid-svg-XtfpNySjtmUxHaL6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XtfpNySjtmUxHaL6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XtfpNySjtmUxHaL6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XtfpNySjtmUxHaL6 .marker.cross{stroke:#333333;}#mermaid-svg-XtfpNySjtmUxHaL6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XtfpNySjtmUxHaL6 p{margin:0;}#mermaid-svg-XtfpNySjtmUxHaL6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 .cluster-label text{fill:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 .cluster-label span{color:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 .cluster-label span p{background-color:transparent;}#mermaid-svg-XtfpNySjtmUxHaL6 .label text,#mermaid-svg-XtfpNySjtmUxHaL6 span{fill:#333;color:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 .node rect,#mermaid-svg-XtfpNySjtmUxHaL6 .node circle,#mermaid-svg-XtfpNySjtmUxHaL6 .node ellipse,#mermaid-svg-XtfpNySjtmUxHaL6 .node polygon,#mermaid-svg-XtfpNySjtmUxHaL6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XtfpNySjtmUxHaL6 .rough-node .label text,#mermaid-svg-XtfpNySjtmUxHaL6 .node .label text,#mermaid-svg-XtfpNySjtmUxHaL6 .image-shape .label,#mermaid-svg-XtfpNySjtmUxHaL6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-XtfpNySjtmUxHaL6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XtfpNySjtmUxHaL6 .rough-node .label,#mermaid-svg-XtfpNySjtmUxHaL6 .node .label,#mermaid-svg-XtfpNySjtmUxHaL6 .image-shape .label,#mermaid-svg-XtfpNySjtmUxHaL6 .icon-shape .label{text-align:center;}#mermaid-svg-XtfpNySjtmUxHaL6 .node.clickable{cursor:pointer;}#mermaid-svg-XtfpNySjtmUxHaL6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XtfpNySjtmUxHaL6 .arrowheadPath{fill:#333333;}#mermaid-svg-XtfpNySjtmUxHaL6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XtfpNySjtmUxHaL6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XtfpNySjtmUxHaL6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XtfpNySjtmUxHaL6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XtfpNySjtmUxHaL6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XtfpNySjtmUxHaL6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XtfpNySjtmUxHaL6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XtfpNySjtmUxHaL6 .cluster text{fill:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 .cluster span{color:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XtfpNySjtmUxHaL6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XtfpNySjtmUxHaL6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-XtfpNySjtmUxHaL6 .icon-shape,#mermaid-svg-XtfpNySjtmUxHaL6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XtfpNySjtmUxHaL6 .icon-shape p,#mermaid-svg-XtfpNySjtmUxHaL6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XtfpNySjtmUxHaL6 .icon-shape .label rect,#mermaid-svg-XtfpNySjtmUxHaL6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XtfpNySjtmUxHaL6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XtfpNySjtmUxHaL6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XtfpNySjtmUxHaL6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
筛选城市类型
查询Attraction
列表展示
收藏或评论
结束

3.2.3 旅游攻略模块设计

用户可发布攻略;AI 规划成功后自动生成带封面与关联景点的攻略。详情页去掉内部 [plan:id] 标记,将 Markdown 转为标题与列表 HTML。列表展示封面、城市、标题与浏览量。手工发布需填写标题与正文,城市可与景点关联。自动攻略与手工攻略共用详情模板,避免两套排版。若规划失败则不写攻略,避免空文案污染列表。

图3.7 旅游攻略模块流程图
#mermaid-svg-YM7KPFmvvWg9fQWc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YM7KPFmvvWg9fQWc .error-icon{fill:#552222;}#mermaid-svg-YM7KPFmvvWg9fQWc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YM7KPFmvvWg9fQWc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YM7KPFmvvWg9fQWc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YM7KPFmvvWg9fQWc .marker.cross{stroke:#333333;}#mermaid-svg-YM7KPFmvvWg9fQWc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YM7KPFmvvWg9fQWc p{margin:0;}#mermaid-svg-YM7KPFmvvWg9fQWc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc .cluster-label text{fill:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc .cluster-label span{color:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc .cluster-label span p{background-color:transparent;}#mermaid-svg-YM7KPFmvvWg9fQWc .label text,#mermaid-svg-YM7KPFmvvWg9fQWc span{fill:#333;color:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc .node rect,#mermaid-svg-YM7KPFmvvWg9fQWc .node circle,#mermaid-svg-YM7KPFmvvWg9fQWc .node ellipse,#mermaid-svg-YM7KPFmvvWg9fQWc .node polygon,#mermaid-svg-YM7KPFmvvWg9fQWc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YM7KPFmvvWg9fQWc .rough-node .label text,#mermaid-svg-YM7KPFmvvWg9fQWc .node .label text,#mermaid-svg-YM7KPFmvvWg9fQWc .image-shape .label,#mermaid-svg-YM7KPFmvvWg9fQWc .icon-shape .label{text-anchor:middle;}#mermaid-svg-YM7KPFmvvWg9fQWc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YM7KPFmvvWg9fQWc .rough-node .label,#mermaid-svg-YM7KPFmvvWg9fQWc .node .label,#mermaid-svg-YM7KPFmvvWg9fQWc .image-shape .label,#mermaid-svg-YM7KPFmvvWg9fQWc .icon-shape .label{text-align:center;}#mermaid-svg-YM7KPFmvvWg9fQWc .node.clickable{cursor:pointer;}#mermaid-svg-YM7KPFmvvWg9fQWc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YM7KPFmvvWg9fQWc .arrowheadPath{fill:#333333;}#mermaid-svg-YM7KPFmvvWg9fQWc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YM7KPFmvvWg9fQWc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YM7KPFmvvWg9fQWc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YM7KPFmvvWg9fQWc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YM7KPFmvvWg9fQWc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YM7KPFmvvWg9fQWc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YM7KPFmvvWg9fQWc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YM7KPFmvvWg9fQWc .cluster text{fill:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc .cluster span{color:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YM7KPFmvvWg9fQWc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YM7KPFmvvWg9fQWc rect.text{fill:none;stroke-width:0;}#mermaid-svg-YM7KPFmvvWg9fQWc .icon-shape,#mermaid-svg-YM7KPFmvvWg9fQWc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YM7KPFmvvWg9fQWc .icon-shape p,#mermaid-svg-YM7KPFmvvWg9fQWc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YM7KPFmvvWg9fQWc .icon-shape .label rect,#mermaid-svg-YM7KPFmvvWg9fQWc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YM7KPFmvvWg9fQWc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YM7KPFmvvWg9fQWc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YM7KPFmvvWg9fQWc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
浏览攻略列表
阅读详情
可选发布攻略
关联景点
结束

3.2.4 AI路线规划模块设计

表单提交城市、天数、预算与偏好。检索只使用本城 Attraction。排程按评分与门票预算选取每日约 2~3 个点。写行程调用智谱,失败用规则文案。随后 enrich_spots 补坐标,保存 PlanRecord,并 publish_guide_from_plan。页面在结果区展示过程日志,标明引擎是 langgraph 还是 pipeline。地图按景点顺序打点,标题带当前城市。候选少于 3 个时日志提示数据偏少,避免用户以为系统坏了。POST 后下拉框必须回填本次城市。

图3.8 AI路线规划模块流程图
#mermaid-svg-Upjc0ztqVoS2gall{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Upjc0ztqVoS2gall .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Upjc0ztqVoS2gall .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Upjc0ztqVoS2gall .error-icon{fill:#552222;}#mermaid-svg-Upjc0ztqVoS2gall .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Upjc0ztqVoS2gall .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Upjc0ztqVoS2gall .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Upjc0ztqVoS2gall .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Upjc0ztqVoS2gall .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Upjc0ztqVoS2gall .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Upjc0ztqVoS2gall .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Upjc0ztqVoS2gall .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Upjc0ztqVoS2gall .marker.cross{stroke:#333333;}#mermaid-svg-Upjc0ztqVoS2gall svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Upjc0ztqVoS2gall p{margin:0;}#mermaid-svg-Upjc0ztqVoS2gall .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Upjc0ztqVoS2gall .cluster-label text{fill:#333;}#mermaid-svg-Upjc0ztqVoS2gall .cluster-label span{color:#333;}#mermaid-svg-Upjc0ztqVoS2gall .cluster-label span p{background-color:transparent;}#mermaid-svg-Upjc0ztqVoS2gall .label text,#mermaid-svg-Upjc0ztqVoS2gall span{fill:#333;color:#333;}#mermaid-svg-Upjc0ztqVoS2gall .node rect,#mermaid-svg-Upjc0ztqVoS2gall .node circle,#mermaid-svg-Upjc0ztqVoS2gall .node ellipse,#mermaid-svg-Upjc0ztqVoS2gall .node polygon,#mermaid-svg-Upjc0ztqVoS2gall .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Upjc0ztqVoS2gall .rough-node .label text,#mermaid-svg-Upjc0ztqVoS2gall .node .label text,#mermaid-svg-Upjc0ztqVoS2gall .image-shape .label,#mermaid-svg-Upjc0ztqVoS2gall .icon-shape .label{text-anchor:middle;}#mermaid-svg-Upjc0ztqVoS2gall .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Upjc0ztqVoS2gall .rough-node .label,#mermaid-svg-Upjc0ztqVoS2gall .node .label,#mermaid-svg-Upjc0ztqVoS2gall .image-shape .label,#mermaid-svg-Upjc0ztqVoS2gall .icon-shape .label{text-align:center;}#mermaid-svg-Upjc0ztqVoS2gall .node.clickable{cursor:pointer;}#mermaid-svg-Upjc0ztqVoS2gall .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Upjc0ztqVoS2gall .arrowheadPath{fill:#333333;}#mermaid-svg-Upjc0ztqVoS2gall .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Upjc0ztqVoS2gall .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Upjc0ztqVoS2gall .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Upjc0ztqVoS2gall .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Upjc0ztqVoS2gall .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Upjc0ztqVoS2gall .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Upjc0ztqVoS2gall .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Upjc0ztqVoS2gall .cluster text{fill:#333;}#mermaid-svg-Upjc0ztqVoS2gall .cluster span{color:#333;}#mermaid-svg-Upjc0ztqVoS2gall div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Upjc0ztqVoS2gall .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Upjc0ztqVoS2gall rect.text{fill:none;stroke-width:0;}#mermaid-svg-Upjc0ztqVoS2gall .icon-shape,#mermaid-svg-Upjc0ztqVoS2gall .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Upjc0ztqVoS2gall .icon-shape p,#mermaid-svg-Upjc0ztqVoS2gall .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Upjc0ztqVoS2gall .icon-shape .label rect,#mermaid-svg-Upjc0ztqVoS2gall .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Upjc0ztqVoS2gall .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Upjc0ztqVoS2gall .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Upjc0ztqVoS2gall :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
输入城市天数预算
检索本城景点
排程选点
大模型写行程
地图展示
结束

3.2.5 旅游数据分析模块设计

接口 /app/api/analysis/options//app/api/analysis/bundle/。bundle 含 KPI、城市 TOP、类型玫瑰、票价面积线、评分柱、时长柱、免费漏斗、散点、高分 TOP、收藏 TOP、雷达、规划城市柱与天数饼。筛选项 version 对齐缓存版本。筛选条件包括城市与类型,改变后重新拉 bundle。免费占比在导入样本中可能超过七成,图表必须把免费单独统计,不能让付费柱看起来像垄断。规划相关图在尚无规划记录时显示空态文案,不报错。

图3.9 旅游数据分析模块流程图
#mermaid-svg-IB3LknPGHPKk2ReJ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IB3LknPGHPKk2ReJ .error-icon{fill:#552222;}#mermaid-svg-IB3LknPGHPKk2ReJ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IB3LknPGHPKk2ReJ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IB3LknPGHPKk2ReJ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IB3LknPGHPKk2ReJ .marker.cross{stroke:#333333;}#mermaid-svg-IB3LknPGHPKk2ReJ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IB3LknPGHPKk2ReJ p{margin:0;}#mermaid-svg-IB3LknPGHPKk2ReJ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ .cluster-label text{fill:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ .cluster-label span{color:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ .cluster-label span p{background-color:transparent;}#mermaid-svg-IB3LknPGHPKk2ReJ .label text,#mermaid-svg-IB3LknPGHPKk2ReJ span{fill:#333;color:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ .node rect,#mermaid-svg-IB3LknPGHPKk2ReJ .node circle,#mermaid-svg-IB3LknPGHPKk2ReJ .node ellipse,#mermaid-svg-IB3LknPGHPKk2ReJ .node polygon,#mermaid-svg-IB3LknPGHPKk2ReJ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IB3LknPGHPKk2ReJ .rough-node .label text,#mermaid-svg-IB3LknPGHPKk2ReJ .node .label text,#mermaid-svg-IB3LknPGHPKk2ReJ .image-shape .label,#mermaid-svg-IB3LknPGHPKk2ReJ .icon-shape .label{text-anchor:middle;}#mermaid-svg-IB3LknPGHPKk2ReJ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IB3LknPGHPKk2ReJ .rough-node .label,#mermaid-svg-IB3LknPGHPKk2ReJ .node .label,#mermaid-svg-IB3LknPGHPKk2ReJ .image-shape .label,#mermaid-svg-IB3LknPGHPKk2ReJ .icon-shape .label{text-align:center;}#mermaid-svg-IB3LknPGHPKk2ReJ .node.clickable{cursor:pointer;}#mermaid-svg-IB3LknPGHPKk2ReJ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IB3LknPGHPKk2ReJ .arrowheadPath{fill:#333333;}#mermaid-svg-IB3LknPGHPKk2ReJ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IB3LknPGHPKk2ReJ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IB3LknPGHPKk2ReJ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IB3LknPGHPKk2ReJ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IB3LknPGHPKk2ReJ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IB3LknPGHPKk2ReJ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IB3LknPGHPKk2ReJ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IB3LknPGHPKk2ReJ .cluster text{fill:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ .cluster span{color:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IB3LknPGHPKk2ReJ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IB3LknPGHPKk2ReJ rect.text{fill:none;stroke-width:0;}#mermaid-svg-IB3LknPGHPKk2ReJ .icon-shape,#mermaid-svg-IB3LknPGHPKk2ReJ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IB3LknPGHPKk2ReJ .icon-shape p,#mermaid-svg-IB3LknPGHPKk2ReJ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IB3LknPGHPKk2ReJ .icon-shape .label rect,#mermaid-svg-IB3LknPGHPKk2ReJ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IB3LknPGHPKk2ReJ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IB3LknPGHPKk2ReJ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IB3LknPGHPKk2ReJ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
加载筛选项
请求analysis bundle
聚合分桶
ECharts渲染
结束

3.2.6 数据爬取与后台管理模块设计

前台 /app/crawl/ 可启动去哪儿爬取或导入样本 CSV。truncate 时会清空景点及攻略、收藏、评论、规划以免演示残留。SimpleUI 维护六类业务数据,保存后 invalidate_travel_cache。爬取受目标站点结构变化影响,答辩应以 CSV 导入为稳定路径。后台菜单把景点、攻略、规划、收藏、评论、前台用户放在「旅游业务」分组。管理员改封面或票价后,前台列表应立即看到新值。

图3.10 数据爬取与后台管理模块流程图
#mermaid-svg-typuxkw3wuHmA38a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-typuxkw3wuHmA38a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-typuxkw3wuHmA38a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-typuxkw3wuHmA38a .error-icon{fill:#552222;}#mermaid-svg-typuxkw3wuHmA38a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-typuxkw3wuHmA38a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-typuxkw3wuHmA38a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-typuxkw3wuHmA38a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-typuxkw3wuHmA38a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-typuxkw3wuHmA38a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-typuxkw3wuHmA38a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-typuxkw3wuHmA38a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-typuxkw3wuHmA38a .marker.cross{stroke:#333333;}#mermaid-svg-typuxkw3wuHmA38a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-typuxkw3wuHmA38a p{margin:0;}#mermaid-svg-typuxkw3wuHmA38a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-typuxkw3wuHmA38a .cluster-label text{fill:#333;}#mermaid-svg-typuxkw3wuHmA38a .cluster-label span{color:#333;}#mermaid-svg-typuxkw3wuHmA38a .cluster-label span p{background-color:transparent;}#mermaid-svg-typuxkw3wuHmA38a .label text,#mermaid-svg-typuxkw3wuHmA38a span{fill:#333;color:#333;}#mermaid-svg-typuxkw3wuHmA38a .node rect,#mermaid-svg-typuxkw3wuHmA38a .node circle,#mermaid-svg-typuxkw3wuHmA38a .node ellipse,#mermaid-svg-typuxkw3wuHmA38a .node polygon,#mermaid-svg-typuxkw3wuHmA38a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-typuxkw3wuHmA38a .rough-node .label text,#mermaid-svg-typuxkw3wuHmA38a .node .label text,#mermaid-svg-typuxkw3wuHmA38a .image-shape .label,#mermaid-svg-typuxkw3wuHmA38a .icon-shape .label{text-anchor:middle;}#mermaid-svg-typuxkw3wuHmA38a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-typuxkw3wuHmA38a .rough-node .label,#mermaid-svg-typuxkw3wuHmA38a .node .label,#mermaid-svg-typuxkw3wuHmA38a .image-shape .label,#mermaid-svg-typuxkw3wuHmA38a .icon-shape .label{text-align:center;}#mermaid-svg-typuxkw3wuHmA38a .node.clickable{cursor:pointer;}#mermaid-svg-typuxkw3wuHmA38a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-typuxkw3wuHmA38a .arrowheadPath{fill:#333333;}#mermaid-svg-typuxkw3wuHmA38a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-typuxkw3wuHmA38a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-typuxkw3wuHmA38a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-typuxkw3wuHmA38a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-typuxkw3wuHmA38a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-typuxkw3wuHmA38a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-typuxkw3wuHmA38a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-typuxkw3wuHmA38a .cluster text{fill:#333;}#mermaid-svg-typuxkw3wuHmA38a .cluster span{color:#333;}#mermaid-svg-typuxkw3wuHmA38a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-typuxkw3wuHmA38a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-typuxkw3wuHmA38a rect.text{fill:none;stroke-width:0;}#mermaid-svg-typuxkw3wuHmA38a .icon-shape,#mermaid-svg-typuxkw3wuHmA38a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-typuxkw3wuHmA38a .icon-shape p,#mermaid-svg-typuxkw3wuHmA38a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-typuxkw3wuHmA38a .icon-shape .label rect,#mermaid-svg-typuxkw3wuHmA38a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-typuxkw3wuHmA38a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-typuxkw3wuHmA38a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-typuxkw3wuHmA38a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
启动爬取或CSV导入
写入Attraction
invalidate缓存
后台CRUD
结束

3.3 数据库设计

3.3.1 数据库关系设计

用户一对多收藏、评论、攻略与规划记录;攻略与景点多对多;规划结果以 JSON 文本保存景点顺序,便于地图回放。不加行程明细表是刻意简化:课程演示更关心一次规划的整体结果,而不是复杂的日程编辑器。JSON 字段可读、可拷贝到论文附录说明,也方便前端直接解析打点。若未来需要按日编辑,可再拆表而不影响现有查询索引策略。

图3.11 数据库表关系图
#mermaid-svg-Pm9qQEUbtzoiA2gR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Pm9qQEUbtzoiA2gR .error-icon{fill:#552222;}#mermaid-svg-Pm9qQEUbtzoiA2gR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Pm9qQEUbtzoiA2gR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Pm9qQEUbtzoiA2gR .marker.cross{stroke:#333333;}#mermaid-svg-Pm9qQEUbtzoiA2gR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Pm9qQEUbtzoiA2gR p{margin:0;}#mermaid-svg-Pm9qQEUbtzoiA2gR .entityBox{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Pm9qQEUbtzoiA2gR .relationshipLabelBox{fill:hsl(80, 100%, 96.2745098039%);opacity:0.7;background-color:hsl(80, 100%, 96.2745098039%);}#mermaid-svg-Pm9qQEUbtzoiA2gR .relationshipLabelBox rect{opacity:0.5;}#mermaid-svg-Pm9qQEUbtzoiA2gR .labelBkg{background-color:rgba(248.6666666666, 255, 235.9999999999, 0.5);}#mermaid-svg-Pm9qQEUbtzoiA2gR .edgeLabel .label{fill:#9370DB;font-size:14px;}#mermaid-svg-Pm9qQEUbtzoiA2gR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edge-pattern-dashed{stroke-dasharray:8,8;}#mermaid-svg-Pm9qQEUbtzoiA2gR .node rect,#mermaid-svg-Pm9qQEUbtzoiA2gR .node circle,#mermaid-svg-Pm9qQEUbtzoiA2gR .node ellipse,#mermaid-svg-Pm9qQEUbtzoiA2gR .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Pm9qQEUbtzoiA2gR .relationshipLine{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Pm9qQEUbtzoiA2gR .marker{fill:none!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-Pm9qQEUbtzoiA2gR .edgeLabel{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Pm9qQEUbtzoiA2gR .edgeLabel .label rect{fill:rgba(232,232,232, 0.8);}#mermaid-svg-Pm9qQEUbtzoiA2gR .edgeLabel .label text{fill:#333;}#mermaid-svg-Pm9qQEUbtzoiA2gR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} has
writes
authors
plans
collected
commented
related
APP_USER
int
id
PK
string
username
string
password
string
address
APP_FAVORITE
APP_COMMENT
APP_GUIDE
APP_PLANRECORD
int
id
PK
string
city
int
days
string
status
text
result_text
APP_ATTRACTION
int
id
PK
string
name
string
city
decimal
ticket_price
float
rating

主要字段与模型一致:Attraction 含封面 URL、经纬度、开放时间与建议游玩时长;PlanRecordintent_jsonprocess_logspots_json 与状态枚举 pending/success/failed。

表级说明如下。app_user 保存前台账号、明文密码(课程演示约定)、性别、所在城市、头像与简介。app_attraction 是主数据表,城市与名称联合索引服务规划检索,is_freeticket_price 服务筛选与分桶,cover_url 来自导入,封面失败时模板改用本地占位图。app_favorite 以外键连接用户与景点,联合唯一约束防止重复收藏。app_comment 保存评论文本与整数评分。app_guide 保存标题、城市、正文、封面与浏览量,并通过多对多表关联景点。app_planrecord 保存一次规划的输入与输出:城市、天数、预算、偏好、状态、行程正文、景点 JSON 与过程日志。规划成功后 publish_guide_from_plan 在攻略正文末尾写入 [plan:主键],同一规划再次打开详情时不会重复发文。导入使用 truncate 时会级联清理攻略、收藏、评论与规划,避免演示库残留旧关系。


4 系统实现

4.0 系统整体实现流程

部署后执行 python manage.py runserver 8100。用户经登录进入首页,再访问各业务模块;写操作触发缓存失效。整体流程与模块流程的差别在于:整体图只保留启动、登录、调用、读写、响应五步,细节放到 4.1 至 4.6。集成部署图则强调环境与迁移。界面实现图强调主题与组件加载顺序。答辩讲解时可先用 4.0 建立全局印象,再进入规划与分析两个重点模块。

图4.0 系统整体实现流程图
#mermaid-svg-S17gKJrSDX8kDjbw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-S17gKJrSDX8kDjbw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-S17gKJrSDX8kDjbw .error-icon{fill:#552222;}#mermaid-svg-S17gKJrSDX8kDjbw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-S17gKJrSDX8kDjbw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-S17gKJrSDX8kDjbw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-S17gKJrSDX8kDjbw .marker.cross{stroke:#333333;}#mermaid-svg-S17gKJrSDX8kDjbw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-S17gKJrSDX8kDjbw p{margin:0;}#mermaid-svg-S17gKJrSDX8kDjbw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-S17gKJrSDX8kDjbw .cluster-label text{fill:#333;}#mermaid-svg-S17gKJrSDX8kDjbw .cluster-label span{color:#333;}#mermaid-svg-S17gKJrSDX8kDjbw .cluster-label span p{background-color:transparent;}#mermaid-svg-S17gKJrSDX8kDjbw .label text,#mermaid-svg-S17gKJrSDX8kDjbw span{fill:#333;color:#333;}#mermaid-svg-S17gKJrSDX8kDjbw .node rect,#mermaid-svg-S17gKJrSDX8kDjbw .node circle,#mermaid-svg-S17gKJrSDX8kDjbw .node ellipse,#mermaid-svg-S17gKJrSDX8kDjbw .node polygon,#mermaid-svg-S17gKJrSDX8kDjbw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-S17gKJrSDX8kDjbw .rough-node .label text,#mermaid-svg-S17gKJrSDX8kDjbw .node .label text,#mermaid-svg-S17gKJrSDX8kDjbw .image-shape .label,#mermaid-svg-S17gKJrSDX8kDjbw .icon-shape .label{text-anchor:middle;}#mermaid-svg-S17gKJrSDX8kDjbw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-S17gKJrSDX8kDjbw .rough-node .label,#mermaid-svg-S17gKJrSDX8kDjbw .node .label,#mermaid-svg-S17gKJrSDX8kDjbw .image-shape .label,#mermaid-svg-S17gKJrSDX8kDjbw .icon-shape .label{text-align:center;}#mermaid-svg-S17gKJrSDX8kDjbw .node.clickable{cursor:pointer;}#mermaid-svg-S17gKJrSDX8kDjbw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-S17gKJrSDX8kDjbw .arrowheadPath{fill:#333333;}#mermaid-svg-S17gKJrSDX8kDjbw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-S17gKJrSDX8kDjbw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-S17gKJrSDX8kDjbw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S17gKJrSDX8kDjbw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-S17gKJrSDX8kDjbw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S17gKJrSDX8kDjbw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-S17gKJrSDX8kDjbw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-S17gKJrSDX8kDjbw .cluster text{fill:#333;}#mermaid-svg-S17gKJrSDX8kDjbw .cluster span{color:#333;}#mermaid-svg-S17gKJrSDX8kDjbw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-S17gKJrSDX8kDjbw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-S17gKJrSDX8kDjbw rect.text{fill:none;stroke-width:0;}#mermaid-svg-S17gKJrSDX8kDjbw .icon-shape,#mermaid-svg-S17gKJrSDX8kDjbw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-S17gKJrSDX8kDjbw .icon-shape p,#mermaid-svg-S17gKJrSDX8kDjbw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-S17gKJrSDX8kDjbw .icon-shape .label rect,#mermaid-svg-S17gKJrSDX8kDjbw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-S17gKJrSDX8kDjbw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-S17gKJrSDX8kDjbw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-S17gKJrSDX8kDjbw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
runserver 8100
用户登录
业务模块调用
MySQL读写
页面响应
结束

4.1 用户认证模块的实现

登录视图读取 POST 的 usernamepasswordUser.objects.get 成功后写入 session 并跳转 /app/home/。访问路径:/app/login//app/register//app/profile//app/changePassword/。错误分支统一返回中文提示,不暴露堆栈。密码字段按课程约定明文比对,论文中明确这是演示简化,正式系统应改为哈希存储。注册成功后可立即进入首页,减少老师验收步骤。

python 复制代码
def login(request):
    if request.method == "GET":
        return render(request, "login.html")
    username = request.POST.get("username")
    password = request.POST.get("password")
    try:
        User.objects.get(username=username, password=password)
        request.session["username"] = username
        return redirect("/app/home/")
    except User.DoesNotExist:
        return errorResponse.errorResponse(request, "用户名或密码错误")

图4.1 用户认证模块实现流程图
#mermaid-svg-X0Quf7LEVECUGJOO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X0Quf7LEVECUGJOO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X0Quf7LEVECUGJOO .error-icon{fill:#552222;}#mermaid-svg-X0Quf7LEVECUGJOO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X0Quf7LEVECUGJOO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X0Quf7LEVECUGJOO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X0Quf7LEVECUGJOO .marker.cross{stroke:#333333;}#mermaid-svg-X0Quf7LEVECUGJOO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X0Quf7LEVECUGJOO p{margin:0;}#mermaid-svg-X0Quf7LEVECUGJOO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X0Quf7LEVECUGJOO .cluster-label text{fill:#333;}#mermaid-svg-X0Quf7LEVECUGJOO .cluster-label span{color:#333;}#mermaid-svg-X0Quf7LEVECUGJOO .cluster-label span p{background-color:transparent;}#mermaid-svg-X0Quf7LEVECUGJOO .label text,#mermaid-svg-X0Quf7LEVECUGJOO span{fill:#333;color:#333;}#mermaid-svg-X0Quf7LEVECUGJOO .node rect,#mermaid-svg-X0Quf7LEVECUGJOO .node circle,#mermaid-svg-X0Quf7LEVECUGJOO .node ellipse,#mermaid-svg-X0Quf7LEVECUGJOO .node polygon,#mermaid-svg-X0Quf7LEVECUGJOO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X0Quf7LEVECUGJOO .rough-node .label text,#mermaid-svg-X0Quf7LEVECUGJOO .node .label text,#mermaid-svg-X0Quf7LEVECUGJOO .image-shape .label,#mermaid-svg-X0Quf7LEVECUGJOO .icon-shape .label{text-anchor:middle;}#mermaid-svg-X0Quf7LEVECUGJOO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X0Quf7LEVECUGJOO .rough-node .label,#mermaid-svg-X0Quf7LEVECUGJOO .node .label,#mermaid-svg-X0Quf7LEVECUGJOO .image-shape .label,#mermaid-svg-X0Quf7LEVECUGJOO .icon-shape .label{text-align:center;}#mermaid-svg-X0Quf7LEVECUGJOO .node.clickable{cursor:pointer;}#mermaid-svg-X0Quf7LEVECUGJOO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X0Quf7LEVECUGJOO .arrowheadPath{fill:#333333;}#mermaid-svg-X0Quf7LEVECUGJOO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X0Quf7LEVECUGJOO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X0Quf7LEVECUGJOO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X0Quf7LEVECUGJOO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X0Quf7LEVECUGJOO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X0Quf7LEVECUGJOO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X0Quf7LEVECUGJOO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X0Quf7LEVECUGJOO .cluster text{fill:#333;}#mermaid-svg-X0Quf7LEVECUGJOO .cluster span{color:#333;}#mermaid-svg-X0Quf7LEVECUGJOO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X0Quf7LEVECUGJOO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X0Quf7LEVECUGJOO rect.text{fill:none;stroke-width:0;}#mermaid-svg-X0Quf7LEVECUGJOO .icon-shape,#mermaid-svg-X0Quf7LEVECUGJOO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X0Quf7LEVECUGJOO .icon-shape p,#mermaid-svg-X0Quf7LEVECUGJOO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X0Quf7LEVECUGJOO .icon-shape .label rect,#mermaid-svg-X0Quf7LEVECUGJOO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X0Quf7LEVECUGJOO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X0Quf7LEVECUGJOO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X0Quf7LEVECUGJOO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
POST /app/login/
查询User表
session写入
redirect home
结束

注册视图校验两次密码、用户名非空与是否重名,成功后同样写入 session。个人中心读取当前 User 记录,改密失败时给出明确中文提示。中间件与视图共同保证未登录不能访问业务页。后台登录不走该中间件,使用 Django 自带表单,账号为 admin/admin123

4.2 景点浏览与收藏模块的实现

filter_attractions 组合城市、类型、免费与模糊搜索。列表分页展示封面卡片。访问路径:/app/attraction//app/attraction/<id>//app/favorites/

python 复制代码
def filter_attractions(request):
    qs = Attraction.objects.all()
    city = (request.GET.get("city") or "").strip()
    q = (request.GET.get("q") or "").strip()
    free = (request.GET.get("free") or "").strip()
    category = (request.GET.get("category") or "").strip()
    if city:
        qs = qs.filter(city=city)
    if category:
        qs = qs.filter(category=category)
    if free == "1":
        qs = qs.filter(is_free=True)
    elif free == "0":
        qs = qs.filter(is_free=False)
    if q:
        qs = qs.filter(
            Q(name__icontains=q) | Q(tags__icontains=q) | Q(intro__icontains=q)
        )
    return qs

详情页读取单条 Attraction,展示简介、门票、评分、开放时间与评论列表,登录用户可提交评论或切换收藏。封面 URL 若为空则使用 /static/img/attraction-placeholder.svg。去哪儿缩略图地址带校验哈希,程序不得擅自改尺寸后缀,否则会 403。

图4.2 景点浏览与收藏模块实现流程图
#mermaid-svg-XgEx3gtjKxNZSbcz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XgEx3gtjKxNZSbcz .error-icon{fill:#552222;}#mermaid-svg-XgEx3gtjKxNZSbcz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XgEx3gtjKxNZSbcz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XgEx3gtjKxNZSbcz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XgEx3gtjKxNZSbcz .marker.cross{stroke:#333333;}#mermaid-svg-XgEx3gtjKxNZSbcz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XgEx3gtjKxNZSbcz p{margin:0;}#mermaid-svg-XgEx3gtjKxNZSbcz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz .cluster-label text{fill:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz .cluster-label span{color:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz .cluster-label span p{background-color:transparent;}#mermaid-svg-XgEx3gtjKxNZSbcz .label text,#mermaid-svg-XgEx3gtjKxNZSbcz span{fill:#333;color:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz .node rect,#mermaid-svg-XgEx3gtjKxNZSbcz .node circle,#mermaid-svg-XgEx3gtjKxNZSbcz .node ellipse,#mermaid-svg-XgEx3gtjKxNZSbcz .node polygon,#mermaid-svg-XgEx3gtjKxNZSbcz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-XgEx3gtjKxNZSbcz .rough-node .label text,#mermaid-svg-XgEx3gtjKxNZSbcz .node .label text,#mermaid-svg-XgEx3gtjKxNZSbcz .image-shape .label,#mermaid-svg-XgEx3gtjKxNZSbcz .icon-shape .label{text-anchor:middle;}#mermaid-svg-XgEx3gtjKxNZSbcz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-XgEx3gtjKxNZSbcz .rough-node .label,#mermaid-svg-XgEx3gtjKxNZSbcz .node .label,#mermaid-svg-XgEx3gtjKxNZSbcz .image-shape .label,#mermaid-svg-XgEx3gtjKxNZSbcz .icon-shape .label{text-align:center;}#mermaid-svg-XgEx3gtjKxNZSbcz .node.clickable{cursor:pointer;}#mermaid-svg-XgEx3gtjKxNZSbcz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-XgEx3gtjKxNZSbcz .arrowheadPath{fill:#333333;}#mermaid-svg-XgEx3gtjKxNZSbcz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-XgEx3gtjKxNZSbcz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-XgEx3gtjKxNZSbcz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XgEx3gtjKxNZSbcz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-XgEx3gtjKxNZSbcz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XgEx3gtjKxNZSbcz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-XgEx3gtjKxNZSbcz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-XgEx3gtjKxNZSbcz .cluster text{fill:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz .cluster span{color:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-XgEx3gtjKxNZSbcz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-XgEx3gtjKxNZSbcz rect.text{fill:none;stroke-width:0;}#mermaid-svg-XgEx3gtjKxNZSbcz .icon-shape,#mermaid-svg-XgEx3gtjKxNZSbcz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-XgEx3gtjKxNZSbcz .icon-shape p,#mermaid-svg-XgEx3gtjKxNZSbcz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-XgEx3gtjKxNZSbcz .icon-shape .label rect,#mermaid-svg-XgEx3gtjKxNZSbcz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-XgEx3gtjKxNZSbcz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-XgEx3gtjKxNZSbcz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-XgEx3gtjKxNZSbcz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
访问attraction
filter_attractions
分页渲染
收藏写入
结束

4.3 旅游攻略模块的实现

guide_render.prepare_guide_display 拆出目的地、天数、预算、偏好,再把正文转为 HTML。访问路径:/app/guide//app/guide/create//app/guide/<id>/

规划同步攻略时先判断规划状态为成功且正文非空,再按规划编号查重,避免重复生成。标题形如「上海3日游 · AI行程」,正文开头写目的地、天数、预算与偏好,封面取行程中第一个带图的景点,并关联相关景点。列表按时间倒序,详情增加浏览量。手工发布与自动生成共用同一套排版。

python 复制代码
def publish_guide_from_plan(plan, spots=None):
    if not plan or plan.status != "success":
        return None
    body = (plan.result_text or "").strip()
    if not body:
        return None
    marker = f"[plan:{plan.id}]"
    existed = Guide.objects.filter(
        user_id=plan.user_id, content__contains=marker
    ).first()
    if existed:
        return existed
    title = f"{plan.city}{plan.days}日游 · AI行程"
    content = (
        f"目的地:{plan.city}|天数:{plan.days}天|预算:约 {plan.budget} 元\n\n"
        + body
        + f"\n\n{marker}"
    )
    guide = Guide.objects.create(
        user=plan.user, title=title, city=plan.city or "", content=content
    )
    return guide

图4.3 旅游攻略模块实现流程图
#mermaid-svg-PiR5ti4rl7MYEgkf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PiR5ti4rl7MYEgkf .error-icon{fill:#552222;}#mermaid-svg-PiR5ti4rl7MYEgkf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PiR5ti4rl7MYEgkf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PiR5ti4rl7MYEgkf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PiR5ti4rl7MYEgkf .marker.cross{stroke:#333333;}#mermaid-svg-PiR5ti4rl7MYEgkf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PiR5ti4rl7MYEgkf p{margin:0;}#mermaid-svg-PiR5ti4rl7MYEgkf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf .cluster-label text{fill:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf .cluster-label span{color:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf .cluster-label span p{background-color:transparent;}#mermaid-svg-PiR5ti4rl7MYEgkf .label text,#mermaid-svg-PiR5ti4rl7MYEgkf span{fill:#333;color:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf .node rect,#mermaid-svg-PiR5ti4rl7MYEgkf .node circle,#mermaid-svg-PiR5ti4rl7MYEgkf .node ellipse,#mermaid-svg-PiR5ti4rl7MYEgkf .node polygon,#mermaid-svg-PiR5ti4rl7MYEgkf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PiR5ti4rl7MYEgkf .rough-node .label text,#mermaid-svg-PiR5ti4rl7MYEgkf .node .label text,#mermaid-svg-PiR5ti4rl7MYEgkf .image-shape .label,#mermaid-svg-PiR5ti4rl7MYEgkf .icon-shape .label{text-anchor:middle;}#mermaid-svg-PiR5ti4rl7MYEgkf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PiR5ti4rl7MYEgkf .rough-node .label,#mermaid-svg-PiR5ti4rl7MYEgkf .node .label,#mermaid-svg-PiR5ti4rl7MYEgkf .image-shape .label,#mermaid-svg-PiR5ti4rl7MYEgkf .icon-shape .label{text-align:center;}#mermaid-svg-PiR5ti4rl7MYEgkf .node.clickable{cursor:pointer;}#mermaid-svg-PiR5ti4rl7MYEgkf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PiR5ti4rl7MYEgkf .arrowheadPath{fill:#333333;}#mermaid-svg-PiR5ti4rl7MYEgkf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PiR5ti4rl7MYEgkf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PiR5ti4rl7MYEgkf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PiR5ti4rl7MYEgkf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PiR5ti4rl7MYEgkf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PiR5ti4rl7MYEgkf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PiR5ti4rl7MYEgkf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PiR5ti4rl7MYEgkf .cluster text{fill:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf .cluster span{color:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PiR5ti4rl7MYEgkf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PiR5ti4rl7MYEgkf rect.text{fill:none;stroke-width:0;}#mermaid-svg-PiR5ti4rl7MYEgkf .icon-shape,#mermaid-svg-PiR5ti4rl7MYEgkf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PiR5ti4rl7MYEgkf .icon-shape p,#mermaid-svg-PiR5ti4rl7MYEgkf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PiR5ti4rl7MYEgkf .icon-shape .label rect,#mermaid-svg-PiR5ti4rl7MYEgkf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PiR5ti4rl7MYEgkf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PiR5ti4rl7MYEgkf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PiR5ti4rl7MYEgkf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
访问guide
查询Guide
Markdown渲染详情
可选创建
结束

4.4 AI路线规划模块的实现

核心入口为 run_travel_plan。检索在本城无数据时返回空列表并写日志,不再按景点名称扩大到全国库。规划页提交后回填城市下拉框,地图标题显示当前城市。访问路径:/app/ai-plan//app/plan/<id>//app/api/ai-plan/。视图将表单参数交规划引擎,再把行程、日志与景点列表写入 PlanRecord;仅成功状态同步攻略。规划详情页复用攻略排版,日标题以卡片显示。大模型超时则退回规则行程;若无可选景点则给出明确提示。

python 复制代码
qs = Attraction.objects.filter(city=city)
if not qs.exists():
    qs = Attraction.objects.filter(city__icontains=city)
if not qs.exists():
    logs.append(f"[search_attractions] 城市「{city}」库中无景点")
    return {**state, "attractions": [], "logs": logs}

图4.4 AI路线规划模块实现流程图
#mermaid-svg-xHsWFliaMcxonycj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xHsWFliaMcxonycj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xHsWFliaMcxonycj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xHsWFliaMcxonycj .error-icon{fill:#552222;}#mermaid-svg-xHsWFliaMcxonycj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xHsWFliaMcxonycj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xHsWFliaMcxonycj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xHsWFliaMcxonycj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xHsWFliaMcxonycj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xHsWFliaMcxonycj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xHsWFliaMcxonycj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xHsWFliaMcxonycj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xHsWFliaMcxonycj .marker.cross{stroke:#333333;}#mermaid-svg-xHsWFliaMcxonycj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xHsWFliaMcxonycj p{margin:0;}#mermaid-svg-xHsWFliaMcxonycj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xHsWFliaMcxonycj .cluster-label text{fill:#333;}#mermaid-svg-xHsWFliaMcxonycj .cluster-label span{color:#333;}#mermaid-svg-xHsWFliaMcxonycj .cluster-label span p{background-color:transparent;}#mermaid-svg-xHsWFliaMcxonycj .label text,#mermaid-svg-xHsWFliaMcxonycj span{fill:#333;color:#333;}#mermaid-svg-xHsWFliaMcxonycj .node rect,#mermaid-svg-xHsWFliaMcxonycj .node circle,#mermaid-svg-xHsWFliaMcxonycj .node ellipse,#mermaid-svg-xHsWFliaMcxonycj .node polygon,#mermaid-svg-xHsWFliaMcxonycj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xHsWFliaMcxonycj .rough-node .label text,#mermaid-svg-xHsWFliaMcxonycj .node .label text,#mermaid-svg-xHsWFliaMcxonycj .image-shape .label,#mermaid-svg-xHsWFliaMcxonycj .icon-shape .label{text-anchor:middle;}#mermaid-svg-xHsWFliaMcxonycj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xHsWFliaMcxonycj .rough-node .label,#mermaid-svg-xHsWFliaMcxonycj .node .label,#mermaid-svg-xHsWFliaMcxonycj .image-shape .label,#mermaid-svg-xHsWFliaMcxonycj .icon-shape .label{text-align:center;}#mermaid-svg-xHsWFliaMcxonycj .node.clickable{cursor:pointer;}#mermaid-svg-xHsWFliaMcxonycj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xHsWFliaMcxonycj .arrowheadPath{fill:#333333;}#mermaid-svg-xHsWFliaMcxonycj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xHsWFliaMcxonycj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xHsWFliaMcxonycj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xHsWFliaMcxonycj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xHsWFliaMcxonycj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xHsWFliaMcxonycj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xHsWFliaMcxonycj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xHsWFliaMcxonycj .cluster text{fill:#333;}#mermaid-svg-xHsWFliaMcxonycj .cluster span{color:#333;}#mermaid-svg-xHsWFliaMcxonycj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xHsWFliaMcxonycj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xHsWFliaMcxonycj rect.text{fill:none;stroke-width:0;}#mermaid-svg-xHsWFliaMcxonycj .icon-shape,#mermaid-svg-xHsWFliaMcxonycj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xHsWFliaMcxonycj .icon-shape p,#mermaid-svg-xHsWFliaMcxonycj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xHsWFliaMcxonycj .icon-shape .label rect,#mermaid-svg-xHsWFliaMcxonycj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xHsWFliaMcxonycj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xHsWFliaMcxonycj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xHsWFliaMcxonycj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
run_travel_plan
LangGraph或流水线
enrich_spots
保存PlanRecord
同步攻略
结束

4.5 旅游数据分析模块的实现

分析数据由 analysis_bundle 一次组装,并经本地缓存缩短重复打开耗时。票价区间由 build_price_buckets 按样本分位数划分。前端并行请求筛选项与图表数据包。访问路径:/app/analysis/

python 复制代码
def analysis_bundle(request) -> dict:
    def build():
        return _analysis_bundle_build(request)
    return _cache_bundle("analysis", request, build, ttl_mult=2)

图4.5 旅游数据分析模块实现流程图
#mermaid-svg-CB42LOPcBR6RSo8S{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-CB42LOPcBR6RSo8S .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-CB42LOPcBR6RSo8S .error-icon{fill:#552222;}#mermaid-svg-CB42LOPcBR6RSo8S .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-CB42LOPcBR6RSo8S .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-CB42LOPcBR6RSo8S .marker{fill:#333333;stroke:#333333;}#mermaid-svg-CB42LOPcBR6RSo8S .marker.cross{stroke:#333333;}#mermaid-svg-CB42LOPcBR6RSo8S svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-CB42LOPcBR6RSo8S p{margin:0;}#mermaid-svg-CB42LOPcBR6RSo8S .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-CB42LOPcBR6RSo8S .cluster-label text{fill:#333;}#mermaid-svg-CB42LOPcBR6RSo8S .cluster-label span{color:#333;}#mermaid-svg-CB42LOPcBR6RSo8S .cluster-label span p{background-color:transparent;}#mermaid-svg-CB42LOPcBR6RSo8S .label text,#mermaid-svg-CB42LOPcBR6RSo8S span{fill:#333;color:#333;}#mermaid-svg-CB42LOPcBR6RSo8S .node rect,#mermaid-svg-CB42LOPcBR6RSo8S .node circle,#mermaid-svg-CB42LOPcBR6RSo8S .node ellipse,#mermaid-svg-CB42LOPcBR6RSo8S .node polygon,#mermaid-svg-CB42LOPcBR6RSo8S .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-CB42LOPcBR6RSo8S .rough-node .label text,#mermaid-svg-CB42LOPcBR6RSo8S .node .label text,#mermaid-svg-CB42LOPcBR6RSo8S .image-shape .label,#mermaid-svg-CB42LOPcBR6RSo8S .icon-shape .label{text-anchor:middle;}#mermaid-svg-CB42LOPcBR6RSo8S .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-CB42LOPcBR6RSo8S .rough-node .label,#mermaid-svg-CB42LOPcBR6RSo8S .node .label,#mermaid-svg-CB42LOPcBR6RSo8S .image-shape .label,#mermaid-svg-CB42LOPcBR6RSo8S .icon-shape .label{text-align:center;}#mermaid-svg-CB42LOPcBR6RSo8S .node.clickable{cursor:pointer;}#mermaid-svg-CB42LOPcBR6RSo8S .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-CB42LOPcBR6RSo8S .arrowheadPath{fill:#333333;}#mermaid-svg-CB42LOPcBR6RSo8S .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-CB42LOPcBR6RSo8S .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-CB42LOPcBR6RSo8S .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CB42LOPcBR6RSo8S .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-CB42LOPcBR6RSo8S .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CB42LOPcBR6RSo8S .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-CB42LOPcBR6RSo8S .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-CB42LOPcBR6RSo8S .cluster text{fill:#333;}#mermaid-svg-CB42LOPcBR6RSo8S .cluster span{color:#333;}#mermaid-svg-CB42LOPcBR6RSo8S div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-CB42LOPcBR6RSo8S .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-CB42LOPcBR6RSo8S rect.text{fill:none;stroke-width:0;}#mermaid-svg-CB42LOPcBR6RSo8S .icon-shape,#mermaid-svg-CB42LOPcBR6RSo8S .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-CB42LOPcBR6RSo8S .icon-shape p,#mermaid-svg-CB42LOPcBR6RSo8S .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-CB42LOPcBR6RSo8S .icon-shape .label rect,#mermaid-svg-CB42LOPcBR6RSo8S .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-CB42LOPcBR6RSo8S .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-CB42LOPcBR6RSo8S .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-CB42LOPcBR6RSo8S :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
并行加载筛选项与数据
读取景点样本
分桶聚合
前端绘多图
结束

本机实测首次聚合约 0.25 秒,再次打开同一条件时可明显加快。analysis_bundle 先取当前筛选下的景点行,再计算景点数、城市数、平均评分、免费占比、付费均价与规划次数等指标;城市排名取前若干名。门票按分位数分档,免费单独统计。接口为 /app/api/analysis/options//app/api/analysis/bundle/,前端图表容器与返回字段一一对应。

4.6 数据爬取与后台管理模块的实现

crawl_job 支持异步进度;CSV 导入可 --truncate。SimpleUI 菜单分组为「旅游业务」与「用户与权限」。访问路径:/app/crawl//admin/

管理命令 import_qunar_csv 读取样本文件写入或更新景点。前台爬取页可启动任务并查看进度。后台保存景点等数据后会刷新统计缓存。

python 复制代码
def invalidate_travel_cache() -> None:
    try:
        cache.incr(CACHE_VERSION_KEY)
    except Exception:
        cache.set(CACHE_VERSION_KEY, 1, None)

管理员由 seed_admin 写入 Django 自带用户表,与前台账号分属不同表。一般使用去哪儿样本导入即可,不建议再用少量演示种子覆盖正式导入结果。若选择清空后导入,关联的攻略、收藏等也会一并清理,操作前需确认。

图4.6 数据爬取与后台管理模块实现流程图
#mermaid-svg-6I7IZ7BfksnnMr2U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6I7IZ7BfksnnMr2U .error-icon{fill:#552222;}#mermaid-svg-6I7IZ7BfksnnMr2U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6I7IZ7BfksnnMr2U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6I7IZ7BfksnnMr2U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6I7IZ7BfksnnMr2U .marker.cross{stroke:#333333;}#mermaid-svg-6I7IZ7BfksnnMr2U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6I7IZ7BfksnnMr2U p{margin:0;}#mermaid-svg-6I7IZ7BfksnnMr2U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U .cluster-label text{fill:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U .cluster-label span{color:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U .cluster-label span p{background-color:transparent;}#mermaid-svg-6I7IZ7BfksnnMr2U .label text,#mermaid-svg-6I7IZ7BfksnnMr2U span{fill:#333;color:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U .node rect,#mermaid-svg-6I7IZ7BfksnnMr2U .node circle,#mermaid-svg-6I7IZ7BfksnnMr2U .node ellipse,#mermaid-svg-6I7IZ7BfksnnMr2U .node polygon,#mermaid-svg-6I7IZ7BfksnnMr2U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6I7IZ7BfksnnMr2U .rough-node .label text,#mermaid-svg-6I7IZ7BfksnnMr2U .node .label text,#mermaid-svg-6I7IZ7BfksnnMr2U .image-shape .label,#mermaid-svg-6I7IZ7BfksnnMr2U .icon-shape .label{text-anchor:middle;}#mermaid-svg-6I7IZ7BfksnnMr2U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6I7IZ7BfksnnMr2U .rough-node .label,#mermaid-svg-6I7IZ7BfksnnMr2U .node .label,#mermaid-svg-6I7IZ7BfksnnMr2U .image-shape .label,#mermaid-svg-6I7IZ7BfksnnMr2U .icon-shape .label{text-align:center;}#mermaid-svg-6I7IZ7BfksnnMr2U .node.clickable{cursor:pointer;}#mermaid-svg-6I7IZ7BfksnnMr2U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6I7IZ7BfksnnMr2U .arrowheadPath{fill:#333333;}#mermaid-svg-6I7IZ7BfksnnMr2U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6I7IZ7BfksnnMr2U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6I7IZ7BfksnnMr2U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6I7IZ7BfksnnMr2U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6I7IZ7BfksnnMr2U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6I7IZ7BfksnnMr2U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6I7IZ7BfksnnMr2U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6I7IZ7BfksnnMr2U .cluster text{fill:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U .cluster span{color:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6I7IZ7BfksnnMr2U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6I7IZ7BfksnnMr2U rect.text{fill:none;stroke-width:0;}#mermaid-svg-6I7IZ7BfksnnMr2U .icon-shape,#mermaid-svg-6I7IZ7BfksnnMr2U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6I7IZ7BfksnnMr2U .icon-shape p,#mermaid-svg-6I7IZ7BfksnnMr2U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6I7IZ7BfksnnMr2U .icon-shape .label rect,#mermaid-svg-6I7IZ7BfksnnMr2U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6I7IZ7BfksnnMr2U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6I7IZ7BfksnnMr2U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6I7IZ7BfksnnMr2U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
crawl页提交
异步任务导入
SimpleUI保存
invalidate_travel_cache
结束

4.7 用户界面实现

主题文件 static/css/travel-theme.css,主色出行蓝 #287dfa,票价橙色仅用于价格文字。首页为白底介绍区加横排实景图,入口与统计合成一条概览,避免大块纯蓝与白卡硬切。图标字体文件位于 static/vendor/bootstrap-icons/fonts/。规划页、攻略详情与分析页共用同一套卡片圆角与阴影,避免每一页另起一套配色。景点卡片保留封面图,无图用占位 SVG。侧栏当前项高亮。登录页保持简洁表单,不堆砌大面积装饰色块。

图4.7 用户界面功能实现流程图
#mermaid-svg-3l322iBQEZNRTNii{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-3l322iBQEZNRTNii .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3l322iBQEZNRTNii .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3l322iBQEZNRTNii .error-icon{fill:#552222;}#mermaid-svg-3l322iBQEZNRTNii .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3l322iBQEZNRTNii .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3l322iBQEZNRTNii .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3l322iBQEZNRTNii .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3l322iBQEZNRTNii .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3l322iBQEZNRTNii .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3l322iBQEZNRTNii .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3l322iBQEZNRTNii .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3l322iBQEZNRTNii .marker.cross{stroke:#333333;}#mermaid-svg-3l322iBQEZNRTNii svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3l322iBQEZNRTNii p{margin:0;}#mermaid-svg-3l322iBQEZNRTNii .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3l322iBQEZNRTNii .cluster-label text{fill:#333;}#mermaid-svg-3l322iBQEZNRTNii .cluster-label span{color:#333;}#mermaid-svg-3l322iBQEZNRTNii .cluster-label span p{background-color:transparent;}#mermaid-svg-3l322iBQEZNRTNii .label text,#mermaid-svg-3l322iBQEZNRTNii span{fill:#333;color:#333;}#mermaid-svg-3l322iBQEZNRTNii .node rect,#mermaid-svg-3l322iBQEZNRTNii .node circle,#mermaid-svg-3l322iBQEZNRTNii .node ellipse,#mermaid-svg-3l322iBQEZNRTNii .node polygon,#mermaid-svg-3l322iBQEZNRTNii .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3l322iBQEZNRTNii .rough-node .label text,#mermaid-svg-3l322iBQEZNRTNii .node .label text,#mermaid-svg-3l322iBQEZNRTNii .image-shape .label,#mermaid-svg-3l322iBQEZNRTNii .icon-shape .label{text-anchor:middle;}#mermaid-svg-3l322iBQEZNRTNii .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3l322iBQEZNRTNii .rough-node .label,#mermaid-svg-3l322iBQEZNRTNii .node .label,#mermaid-svg-3l322iBQEZNRTNii .image-shape .label,#mermaid-svg-3l322iBQEZNRTNii .icon-shape .label{text-align:center;}#mermaid-svg-3l322iBQEZNRTNii .node.clickable{cursor:pointer;}#mermaid-svg-3l322iBQEZNRTNii .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3l322iBQEZNRTNii .arrowheadPath{fill:#333333;}#mermaid-svg-3l322iBQEZNRTNii .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3l322iBQEZNRTNii .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3l322iBQEZNRTNii .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3l322iBQEZNRTNii .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3l322iBQEZNRTNii .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3l322iBQEZNRTNii .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3l322iBQEZNRTNii .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3l322iBQEZNRTNii .cluster text{fill:#333;}#mermaid-svg-3l322iBQEZNRTNii .cluster span{color:#333;}#mermaid-svg-3l322iBQEZNRTNii div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-3l322iBQEZNRTNii .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3l322iBQEZNRTNii rect.text{fill:none;stroke-width:0;}#mermaid-svg-3l322iBQEZNRTNii .icon-shape,#mermaid-svg-3l322iBQEZNRTNii .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3l322iBQEZNRTNii .icon-shape p,#mermaid-svg-3l322iBQEZNRTNii .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3l322iBQEZNRTNii .icon-shape .label rect,#mermaid-svg-3l322iBQEZNRTNii .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3l322iBQEZNRTNii .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3l322iBQEZNRTNii .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3l322iBQEZNRTNii :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
加载Bootstrap侧栏
应用travel-theme
渲染卡片与图表
结束

图4.9 用户登录界面

图4.10 系统首页界面

图4.11 景点浏览界面

图4.12 景点详情界面

图4.13 旅游攻略列表界面

图4.14 攻略详情界面

图4.15 AI路线规划界面

图4.16 旅游数据分析界面

图4.17 景点爬取界面

图4.18 SimpleUI后台界面

4.8 系统集成与部署

安装依赖后创建库 travel_plan,执行迁移并初始化后台管理员,再导入景点样本。在 .env 中配置智谱密钥,高德密钥按需配置。启动命令为 python manage.py runserver 8100。建议同时提供 travel_plan.sql 便于还原。Windows 环境需注意控制台编码与中文路径。依赖以 requirements.txt 为准,langgraph 为可选组件。建议按登录、首页、景点筛选、路线规划、攻略详情、数据分析、后台管理的顺序检查。导入异常时优先检查 MySQL 服务与库名;图标异常时检查本地字体文件;规划城市不一致时检查表单回填与检索日志。

图4.8 系统集成与部署功能实现流程图
#mermaid-svg-lvctbUp9lEVAQwHW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-lvctbUp9lEVAQwHW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-lvctbUp9lEVAQwHW .error-icon{fill:#552222;}#mermaid-svg-lvctbUp9lEVAQwHW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-lvctbUp9lEVAQwHW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-lvctbUp9lEVAQwHW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-lvctbUp9lEVAQwHW .marker.cross{stroke:#333333;}#mermaid-svg-lvctbUp9lEVAQwHW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-lvctbUp9lEVAQwHW p{margin:0;}#mermaid-svg-lvctbUp9lEVAQwHW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-lvctbUp9lEVAQwHW .cluster-label text{fill:#333;}#mermaid-svg-lvctbUp9lEVAQwHW .cluster-label span{color:#333;}#mermaid-svg-lvctbUp9lEVAQwHW .cluster-label span p{background-color:transparent;}#mermaid-svg-lvctbUp9lEVAQwHW .label text,#mermaid-svg-lvctbUp9lEVAQwHW span{fill:#333;color:#333;}#mermaid-svg-lvctbUp9lEVAQwHW .node rect,#mermaid-svg-lvctbUp9lEVAQwHW .node circle,#mermaid-svg-lvctbUp9lEVAQwHW .node ellipse,#mermaid-svg-lvctbUp9lEVAQwHW .node polygon,#mermaid-svg-lvctbUp9lEVAQwHW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-lvctbUp9lEVAQwHW .rough-node .label text,#mermaid-svg-lvctbUp9lEVAQwHW .node .label text,#mermaid-svg-lvctbUp9lEVAQwHW .image-shape .label,#mermaid-svg-lvctbUp9lEVAQwHW .icon-shape .label{text-anchor:middle;}#mermaid-svg-lvctbUp9lEVAQwHW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-lvctbUp9lEVAQwHW .rough-node .label,#mermaid-svg-lvctbUp9lEVAQwHW .node .label,#mermaid-svg-lvctbUp9lEVAQwHW .image-shape .label,#mermaid-svg-lvctbUp9lEVAQwHW .icon-shape .label{text-align:center;}#mermaid-svg-lvctbUp9lEVAQwHW .node.clickable{cursor:pointer;}#mermaid-svg-lvctbUp9lEVAQwHW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-lvctbUp9lEVAQwHW .arrowheadPath{fill:#333333;}#mermaid-svg-lvctbUp9lEVAQwHW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-lvctbUp9lEVAQwHW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-lvctbUp9lEVAQwHW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lvctbUp9lEVAQwHW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-lvctbUp9lEVAQwHW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lvctbUp9lEVAQwHW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-lvctbUp9lEVAQwHW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-lvctbUp9lEVAQwHW .cluster text{fill:#333;}#mermaid-svg-lvctbUp9lEVAQwHW .cluster span{color:#333;}#mermaid-svg-lvctbUp9lEVAQwHW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-lvctbUp9lEVAQwHW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-lvctbUp9lEVAQwHW rect.text{fill:none;stroke-width:0;}#mermaid-svg-lvctbUp9lEVAQwHW .icon-shape,#mermaid-svg-lvctbUp9lEVAQwHW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-lvctbUp9lEVAQwHW .icon-shape p,#mermaid-svg-lvctbUp9lEVAQwHW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-lvctbUp9lEVAQwHW .icon-shape .label rect,#mermaid-svg-lvctbUp9lEVAQwHW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-lvctbUp9lEVAQwHW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-lvctbUp9lEVAQwHW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-lvctbUp9lEVAQwHW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 开始
配置settings与env
迁移并导SQL
runserver 8100
验收登录规划分析
结束


5 系统测试

5.1 系统功能测试

测试环境为 Windows 10、Python 3.11.4、Django 3.1、MySQL travel_plan、浏览器访问 http://127.0.0.1:8100。前台 admin/123456,后台 admin/admin123。测试采用黑盒步骤:按表格操作并记录是否与预期一致。未使用自动化测试框架,以便与课程答辩的手工演示一致。规划用例刻意包含数据充足城市与数据稀少城市两类,防止只测快乐路径。分析用例要求悬停查看系列名。界面用例检查图标字体与规划回填。集成用例检查根路径跳转与静态资源本地化。

5.1.1 用户认证功能测试用例
编号 说明 条件 操作步骤 预期结果 实际结果
TC-A1 正确登录 库中存在 admin 打开 /app/login/ 输入 admin/123456 提交 进入首页 通过
TC-A2 错误密码 同上 输入错误密码 提示用户名或密码错误 通过
TC-A3 未登录拦截 清空会话 直接访问 /app/home/ 跳转登录页 通过
TC-A4 注册重名 admin 已存在 注册同名 提示账号已存在 通过
5.1.2 数据管理功能测试用例
编号 说明 条件 操作步骤 预期结果 实际结果
TC-D1 景点筛选 已导入景点 景点页选城市并搜索 列表匹配条件 通过
TC-D2 收藏 已登录 详情页收藏 收藏列表出现该景点 通过
TC-D3 CSV导入 样本 CSV 存在 爬取页导入或管理命令 景点数增加且缓存刷新 通过
TC-D4 后台改景点 SimpleUI 登录 修改一条景点后刷新前台 前台数据已更新 通过
5.1.3 核心业务功能测试用例
编号 说明 条件 操作步骤 预期结果 实际结果
TC-B1 本城规划 上海有足够景点 规划页选上海提交 地图与列表均为上海景点 通过
TC-B2 少数据城市 七台河仅 2 个景点 选七台河规划 仅本城少量点,提示景点偏少 通过
TC-B3 规划写攻略 规划成功 打开攻略列表 新增一篇同步攻略 通过
TC-B4 攻略排版 存在 Markdown 正文 打开攻略详情 标题列表渲染,无 ### 裸露 通过
5.1.4 数据分析功能测试用例
编号 说明 条件 操作步骤 预期结果 实际结果
TC-C1 bundle 非空 已导入景点 打开 /app/analysis/ KPI 与多图有数据 通过
TC-C2 TOP 排序 城市 TOP 图 观察横条 最大值在顶部 通过
TC-C3 分桶 查看票价图 对照 note 免费单独统计,无单柱吞全部付费 通过
TC-C4 二次刷新 同筛选再进页 再打开分析页 明显快于首次(缓存) 通过
5.1.5 系统集成功能测试用例
编号 说明 条件 操作步骤 预期结果 实际结果
TC-I1 根路径 服务已启动 访问 / 跳转 /app/login/ 通过
TC-I2 后台菜单 admin123 登录 /admin/ 查看旅游业务菜单 景点攻略等入口可用 通过
TC-I3 静态本地 断外网(除高德) 打开首页 Bootstrap 与图标正常 通过
5.1.6 用户界面功能测试用例
编号 说明 条件 操作步骤 预期结果 实际结果
TC-U1 侧栏图标 已登录 逐项点菜单 图标显示不为方块 通过
TC-U2 首页层次 打开首页 观察页头 白底介绍,非整屏纯蓝硬切 通过
TC-U3 规划回填 规划提交后 看城市下拉 保持刚规划的城市 通过
TC-U4 攻略日卡片 打开同步攻略详情 查看正文区域 日标题成卡片,列表可读 通过
TC-A5 退出登录 已登录 退出后访问业务页 跳转登录 通过
TC-B5 规划日志 规划成功 查看过程日志 含引擎名与检索条数 通过

上述用例覆盖认证、数据、规划、分析、集成与界面六类。实际结果列记"通过"表示在本机环境按步骤操作时符合预期;若老师机器未配置智谱密钥,规划用例仍应以规则行程或明确错误提示可理解为准,不把必须联网作为唯一合格条件。

5.2 系统性能测试

5.2.1 响应时间性能分析

景点约 4900 条时,分析 bundle 首次聚合约 0.25 秒,LocMem 命中后约 0.001 秒。登录后 HTTP 再取 bundle 约 10~20 毫秒量级(含网络)。首页统计走 home_stats 短 TTL 缓存,避免每次 COUNT。规划接口耗时主要取决于智谱往返,本机流水线检索与排程通常在一秒内完成;若触发地理编码,会按缺坐标景点逐条请求,耗时随点数增加。列表分页把每页卡片数限制在可接受范围,封面图失败不影响其它卡片渲染。

5.2.2 并发性能测试

本系统面向课程演示与单人操作,未搭建专用压测环境。智谱免费接口有调用频率限制,规划页宜提交后等待结果。分析页通过缓存与并行请求缩短等待。同一进程内再次打开分析页通常更快。登录与列表不依赖外网大模型,适合连续演示。文中性能数据均来自本机观察,不作不可复现的压测结论。

5.2.3 数据库性能测试

列表筛选利用城市与名称相关索引。分析查询分批读取,散点图抽样控制在约 400 个点,减轻浏览器压力。规划检索对本城最多取 40 个候选后再排程。收藏与评论按外键查询。导入在管理命令或异步任务中执行,避免阻塞普通浏览。实践中,耗时更多来自大模型与地理编码请求,因此优化重点放在缓存与抽样,而不是分库分表。

5.2.4 性能优化措施

主要措施包括:写入后抬升缓存版本、分析查询避免无谓排序、适当延长分析缓存时间、前端在版本变化时刷新图表、静态资源带版本参数、封面失败时使用本地占位图。导入与后台保存共用失效入口。地图脚本按需加载。筛选条件变化时重新取数,相同条件则尽量复用缓存。上述优化面向本机演示,不宣称生产级并发能力。


6 总结与展望

6.1 总结

本文实现了基于 Django 3.1 与 LangGraph 风格编排的智能旅游规划系统。系统运行于端口 8100 ,数据库 travel_plan ,六大功能模块覆盖认证、景点、攻略、规划、分析与导入后台。景点规模约 4934 条、城市约 339 座。规划检索锁定本城,成功记录写入规划表并同步攻略;分析页提供十余种图表并具备缓存。静态资源本地化,大模型密钥不进论文。测试用例覆盖登录、筛选、规划、分析与界面回填,结果满足毕业设计演示要求。

实现过程中重点解决了几类实际问题:规划结果与所选城市不一致时,通过表单回填、本城检索与地图标题提示加以纠正;攻略数量长期为 0 时,在规划成功后同步生成攻略;分析页二次打开偏慢时,引入短时缓存与版本失效;图标显示异常或外链依赖不稳定时,改为本地静态资源。上述指标均可在本机数据库与页面中核对。总结强调可核对,是为了和纯文案型作业区分:端口、库名、景点数、模块数都能当场演示。这也是第 5 章测试用例与第 4 章访问路径写得较具体的原因。

6.2 展望

后续可增加交通与住宿约束、规划失败的结构化原因展示、Redis 替换 LocMem,以及更严格的密码哈希。地图在无 Key 环境下可进一步降级为纯列表。数据导入可增加城市配额说明,避免小城市样本过少影响演示预期。攻略正文可引入更完整的 Markdown 渲染库,规划节点可增加天气或开放日约束。若课程允许,也可把 LangGraph 检查点持久化,便于回放一次规划的中间状态。当前实现边界以本科毕业设计可讲清、可运行为准。

后续可优先完善密码哈希与会话安全,补充规划失败原因说明,并尽量均衡各城市样本;再考虑交通接驳等扩展。可视化可增加导出或简单报表,分析页仍保持一次返回多图。规划流程也可增加人工确认,让用户在生成文案前勾选景点,减少模型自由发挥带来的偏差。上述设想建立在现有六模块之上。


参考文献

1 Django Software Foundation. Django documentationEB/OL. https://docs.djangoproject.com/

2 LangChain. LangGraph documentationEB/OL. https://langchain-ai.github.io/langgraph/

3 智谱AI. 开放平台接口说明EB/OL. https://open.bigmodel.cn/

4 Apache ECharts. HandbookEB/OL. https://echarts.apache.org/handbook/zh/

5 高德开放平台. Web服务 APIEB/OL. https://lbs.amap.com/

6 去哪儿网. 旅游景点信息服务EB/OL. https://www.qunar.com/

7 王珊, 萨师煊. 数据库系统概论M. 北京: 高等教育出版社.

8 韦恩·史蒂文斯. Python编程:从入门到实践M. 北京: 人民邮电出版社.


致谢

时光匆匆,毕业设计接近尾声。首先感谢指导教师在选题确定、需求边界把握以及论文章节安排上给予的耐心指导,使我在开发过程中少走了许多弯路。感谢各位任课老师四年来传授的专业知识,为完成本系统打下了基础。感谢同学在系统试用、界面观感与答辩准备方面提出的意见。撰写论文时还参考了 Django、LangGraph、智谱开放平台与 ECharts 等公开资料,在此一并致谢。由于本人水平有限,文中难免存在不足之处,恳请各位老师批评指正。

相关推荐
青 春 记 忆1 小时前
零基础入门python65:FastAPI 安全调用大模型API
python·fastapi·后端开发
维克兜率天2 小时前
【维克】特征归一化与标准化:为什么模型对数据的“尺度“很敏感?
人工智能·笔记·python·机器学习·量化
2601_962298672 小时前
Python multiprocessing PicklingError: Can&#39;t pickle &l
python·module·multiprocessing·function·picklingerror
CV山月2 小时前
大模型强化学习对齐:从 RLHF 框架到 PPO 算法原理
人工智能·python·大模型·强化学习·多模态·研究生
用户739548002062 小时前
本地 CSV 清洗的小细节:先标准化,再去重,并保留可检查的报告
python
AC赳赳老秦3 小时前
文旅市场公开数据分析:基于 OpenClaw 采集景区客流与门票公示数据,生成区域文旅热度监测报告
java·c语言·python·php·symfony·deepseek·openclaw
青 春 记 忆3 小时前
零基础入门python68:FastAPI 完整博客运行与项目验收
python·fastapi·后端开发
鹿鹿学长3 小时前
发题前三天,组委会在过三道关:赛题七渠道首发、知网统一收卷、AI 详情 PDF 首进支撑材料
python·自动化
geovindu3 小时前
python: Face Recognition
开发语言·后端·python·人脸识别