最近有不少准备做毕设的同学来问同一个题目:“基于Java+SSM+Django的个性化旅游攻略定制系统”。说实话,第一次看到这个标题我就知道这活儿不简单——它同时把两套后端技术栈塞进了一个项目里,而且核心功能是“个性化推荐”,不是单纯做个增删改查的旅游网站。这篇文章我就把自己在这个项目上的完整落地过程梳理一遍,包括需求怎么拆、SSM和Django怎么分工、行程生成算法到底怎么写、联调和交付时要注意哪些坑。不管你是正在选毕设题目,还是已经开始动手写代码,按这条线走完,你应该能对这个项目有非常清晰的认识。
1. 先说清楚这个系统到底是做什么的
很多同学一看到“旅游攻略定制系统”,第一反应就是“这不就是个旅游网站吗?景点列表、攻略文章、评论收藏,老三样”。如果你真这么想,那做出来的东西大概率只是个旅游CMS,和题目里的“个性化”“定制”完全不沾边。这个项目最核心的价值,不在“展示攻略”,而在“定制行程”。
1.1 传统旅游网站的痛点,恰恰是这个系统的切入点
市面上的旅游平台,攻略基本都是以“目的地”为单位组织起来的,比如“成都三日游攻略”,内容写得再细,也是一个固定的模板,不会因为你喜欢博物馆、讨厌网红店、带着老人走不动路而有任何变化。用户看到一篇攻略,还得自己手动查地图、算时间、排路线、估预算,整个过程又繁琐又容易翻车。
而这个系统要解决的,就是“让用户描述自己的偏好,然后自动生成一份按天排好、有景点有餐饮、能控制预算的行程攻略”。用户不用再自己做行程整合,系统替他完成“信息筛选”和“路线编排”这两件最费神的事。
1.2 功能模块的完整拆解
从产品功能上看,这个系统整体可以拆成三个端:
- 用户端:注册登录、个性化偏好设置(选择感兴趣的标签、预算区间、游玩节奏)、浏览景点和攻略、一键生成个性化行程、查看/收藏/调整行程、发表评论和收藏景点。
- 管理端:景点信息管理(名称、介绍、门票、经纬度、图片、标签)、攻略文章管理、用户管理、标签体系管理、行程数据统计。管理端是典型的SSM后端+管理后台页面的组合。
- 推荐服务端:接收用户的偏好数据,计算景点匹配度,按天分配形成行程,并把结果返回给主业务系统。这一部分我建议用Django独立部署,后面会详细讲。
从开发角度看,它其实是一个“主业务系统+独立推荐服务”的双服务架构。主业务系统负责账号、数据维护、页面交互,推荐服务负责核心算法。
1.3 一次完整的用户旅程
如果站在用户体验的角度,整个流程是这样跑的:
- 用户注册登录,进入个人中心,选择自己感兴趣的标签,比如“自然风光”“历史古迹”“美食探店”“亲子友好”“低消费穷游”。
- 用户浏览景点列表,看到喜欢的景点可以收藏,这些行为也会被记入偏好画像。
- 用户点击“智能生成行程”,输入出游天数(比如3天)、预算上限(比如人均1500元/天)、出发地(用于判断往返时间),提交生成请求。
- 推荐服务根据用户画像,从景点库中筛选出匹配度最高的景点集合,再按照天数和地理位置,把这些景点排成“第1天上午X景区、中午Y餐厅、下午Z景区……”的完整行程。
- 行程生成后,用户可以手动调整顺序,或者换掉个别景点,调整行为会被记为反馈,用于下次生成时修正权重。
- 用户确认行程,可以选择收藏,也可以生成一份图文攻略分享给朋友。
这套流程听起来不复杂,但真正落地的时候,难点全在“第4步”的行程生成算法上,第2、3步的偏好采集和第5步的反馈修正,也都不是普通CRUD可以糊弄过去的。我后面会专门用一章把算法逻辑展开。
2. 双后端架构:SSM和Django到底怎么分工
如果你去搜这个题目的参考文献,会发现很多论文里只写了“基于SSM的旅游网站”,而不会出现Django。那为什么这个项目标题里要同时出现Java+SSM和Django?在我看来,这其实是这类系统在落地时最常见、也最合理的工程化处理方式:将核心业务和推荐算法拆成两个独立服务,用不同的技术栈分别实现。
2.1 为什么不是“二选一”,而是“两套一起用”
先回答一个很多人会问的问题:一个系统里出现两套后端框架,是不是多余?
如果你做的只是一个普通的旅游信息展示网站,那确实多余。但“个性化旅游攻略定制系统”里最重的部分是“个性化定制”,这一步通常涉及大量数据处理和算法逻辑。SSM这套组合强在业务系统开发——Spring管对象和事务,SpringMVC管请求分发,MyBatis管数据库操作,做管理系统和业务接口非常顺手。但如果要在Java里写偏好的相似度计算、按条件做行程分组规划,不仅代码冗长,而且迭代起来很费劲。
Python的Django则正好补上这块短板:自带ORM、自带Admin后台、处理数据计算和算法逻辑非常灵活,实现一个推荐函数往往只需要几十行代码。所以最合理的分工就是:SSM负责主业务+管理端,Django独立部署为推荐服务,两边通过HTTP接口通信,各干各擅长的活。
这种“Java做主应用、Python做算法服务”的架构,在真实企业里也很常见,不是毕设为了凑技术栈硬拼。答辩的时候你只要把设计理由讲清楚,老师会认为你是有工程意识的。
2.2 SSM三件套在这个项目里分别做了什么
先简单过一遍SSM的分工,这个你答辩时大概率会被问到。
- Spring:负责统一管理Service层和DAO层的Bean对象,声明式事务。比如用户提交偏好设置的接口,要同时更新user表、user_tag表、如果用户之前有行程记录还要标记其画像过期,这些操作必须在同一个事务里,否则数据写到一半失败就麻烦了。
- SpringMVC:负责接收前端请求、路由映射、参数绑定。比如
/api/user/profile这个接口进来,SpringMVC把请求体里的JSON绑定成UserProfileDTO对象,再调用Service层处理。 - MyBatis:负责SQL操作。个性化推荐需要灵活的多表查询,比如“查一个用户的所有偏好的同时,联查出每个标签对应的景点集合”,MyBatis写动态SQL很方便,尤其是
<foreach>、<if>这类标签,比JPA那种“什么都自动”的方式更容易控制最终执行的SQL。
建议把SSM端的代码按标准分层来组织:Controller -> Service -> ServiceImpl -> Mapper,这样答辩讲起来很清晰,评阅老师看代码也省力。
2.3 Django端扮演的角色
Django在这个项目里不是做一个完整网站,而是作为一个独立的推荐服务存在。它主要提供两组接口:
- 偏好画像接口:接收用户ID,从数据库读取该用户的所有标签、收藏记录、历史行程调整记录,整理成一份结构化的偏好向量。
- 行程生成接口:这是核心。接收用户ID、天数、预算、节奏偏好,执行推荐算法,返回按天组织的行程JSON。
有人会问,Django也能连MySQL吗?当然可以,Django的ORM支持MySQL,你只需要在settings.py里配置好数据库连接,然后把SSM端的user表、scenic表、user_tag表当成普通的数据表去读就行。两边操作同一个数据库,不会出现数据不一致的问题。
之所以不用Flask而用Django,主要是因为它自带ORM和Admin后台。你可以直接用Django Admin管理景点数据、标签数据,甚至调试中间结果,很多杂活不用额外开发管理页面。
2.4 两个服务之间的通信与鉴权
两个服务通信,最容易踩坑的是“鉴权”和“跨域”两个问题。我的做法是这样的:
- 用户在SSM端登录成功后,SSM签发一个JWT Token返回给前端。
- 前端调用Django推荐服务时,把Token放在请求头里带上。
- Django服务写一个中间件,收到请求后校验Token的签名和有效期。最简单的做法是:Django和SSM共享同一个JWT密钥,两边用同一个签名算法,Django只负责验签,不重新发Token。
- 校验通过后,Django从Token里解析出userId,再去数据库查用户画像。
这里有一个非常关键的点:Django服务不用自己维护用户session,它只认Token里的用户ID。也就是说,登录状态完全由SSM端管理,Django端是无状态的,这样两个服务之间就不存在session共享问题。
跨域问题也很常见。当前端页面在8080端口(SSM),而推荐服务跑在8000端口(Django),浏览器直接发Ajax请求肯定会被跨域拦截。解决方式是在Django里加一个CORS中间件,允许来自前端域名的跨域请求。一个简单的实现像这样:
class SimpleCorsMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): response = self.get_response(request) response["Access-Control-Allow-Origin"] = "http://localhost:8080" response["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" response["Access-Control-Allow-Headers"] = "Content-Type, Authorization" return response如果你熟悉django-cors-headers这个库,也可以直接用现成的配置,效果一样。
3. 数据库设计:支撑个性化定制的核心表结构
数据库设计决定这个系统能走多远。很多同学图省事,把所有景点标签塞进一个逗号分隔的字段里,这样后面写推荐算法的时候会痛苦到怀疑人生。我直接把核心表结构过一次,并解释每张表为什么这么设计。
3.1 核心表清单
| 表名 | 用途 | 关键字段 |
|---|---|---|
| user | 用户表 | id, username, password, avatar, budget_level, travel_rhythm |
| tag | 标签表 | id, name, category |
| user_tag | 用户偏好标签关联表 | id, user_id, tag_id, weight |
| scenic | 景点表 | id, name, description, address, lng, lat, ticket_price, duration_hours, cover_img, status |
| scenic_tag | 景点标签关联表 | id, scenic_id, tag_id |
| article | 攻略文章表 | id, title, author_id, content, scenic_ids, create_time |
| itinerary | 行程主表 | id, user_id, title, days, total_budget, status, create_time |
| itinerary_day | 行程明细表 | id, itinerary_id, day_number, scenic_id, start_time, duration_hours, transport_tip, note |
| favorite | 收藏表 | id, user_id, scenic_id, create_time |
| comment | 评论表 | id, user_id, scenic_id, content, create_time, reply_id |
3.2 用户偏好为什么要单独拆表
有些项目偷懒,在user表里加一个preference字段,存“自然风光,美食,亲子”这样的字符串。当时看着省事,等推荐算法写起来就傻眼了——你想统计“有多少用户喜欢自然风光”,得把每个用户的字符串split出来再统计,性能差且代码丑陋。
单独抽一张user_tag关联表,好处是本质上的“一对多”关系被正规化了:一个用户可以有多个tag,每个tag对应一个权重。这个权重很重要,比如用户选了“美食探店”,又经常收藏美食类景点,权重就会从默认的1.0慢慢升到1.5。推荐算法在算匹配度时,权重高的标签对分数贡献更大,这就实现了“偏好不只是标签,还有强度”。
3.3 行程主表和行程明细表的设计逻辑
生成一份行程,不是简单地在itinerary表里存一个“景点列表”完事。我们需要支持“按天展示”“调整某一天的景点”“重新排序”这些操作,所以行程至少要拆成两张表:
- itinerary主表:记录这一份行程的整体信息,比如属于哪个用户、总共几天、标题、总预算、状态(草稿/已确认/已完成)。
- itinerary_day明细表:每一行代表“某一天的某个景点安排”,包含第几天、景点ID、建议游玩时长、交通提示。
这种设计的好处非常直观:前端渲染“第1天”的行程,只需要where itinerary_id = ? and day_number = 1按顺序查询。用户想调整第2天和第3天的景点,也只需要更新明细表的day_number。如果用一个字段存JSON数组,表面省事,后面做更新和统计都会变成灾难。
3.4 景点经纬度和时长字段是关键
很多低配版旅游系统在设计scenic表的时候,连经纬度都不加。但行程生成算法有一个硬约束——同一天内的景点在地理位置上应该尽量靠近,否则系统生成的行程可能上午让你在城东爬山,下午让你去城西逛博物馆,车程两小时,完全不可执行。
所以在scenic表里,我强烈建议加两个字段:lng、lat(经纬度),另外再加一个duration_hours(建议游玩时长)。这两个字段是行程分组算法的基础输入,也是体现“可执行性”的关键细节。答辩时提到“考虑了地理位置约束”,老师会认为你的系统是真正以用户落地体验为导向的。
4. 行程生成引擎:从用户偏好到一份可执行的攻略
这是整个系统最有技术含量、也最值得在文档里重点展开的部分。行程生成引擎不是一个“推荐两个景点完事”的功能,它要做的事情是:从几百个景点里,选出一批匹配用户偏好的景点,再按天把它排成一个时间、地理、预算上都合理的行程。
4.1 算法选型:先做可解释的,别上来就搞深度学习
有些同学一看到“个性化”,就想上协同过滤、上深度学习。我的建议是:作为毕设项目,优先选择可解释、可调整、效果可控的标签匹配算法。
原因有三个。第一,答辩的时候老师一定会问“你的推荐依据是什么”,基于标签的算法每一步都能说清楚:你的标签是A和B,这个景点带标签A,所以匹配度加X分。黑盒模型很难讲清。第二,标签匹配算法的效果在中小规模数据集上并不输给复杂模型,而且不需要训练时间。第三,系统刚上线没有用户行为数据,协同过滤根本无法冷启动,但标签匹配天生不需要冷启动,用户只要选了标签就能立刻出结果。
4.2 偏好匹配度计算
核心计算逻辑是给每个候选景点打一个“用户偏好分”。我用的公式是这样的:
score(user, scenic) = sum(weight_i * match_i) / sum(weight_i)其中weight_i是用户第i个偏好标签的权重,match_i是景点是否是第i个标签。如果景点带这个标签,match为1,否则为0。这样一来,一个景点如果同时匹配了用户权重最高的“自然风光”和“徒步”标签,它的分数就会明显高于只匹配了一个冷门标签的景点。
在基础分之上,还可以叠加几个修正因子:
- 热度修正:景点本身的热度系数乘以一个小权重,避免总推冷门景点。
- 预算过滤:如果用户预算等级是“穷游”,就把门票价格超过某个阈值的景点直接过滤掉。
- 节奏修正:用户在偏好设置里选了“慢节奏”,那么每天安排的景点数量就要限制在2~3个,而不是3~4个。
在Django里实现这部分,逻辑很直白:
def calc_match_score(user_weights, scenic_tags): total_weight = 0.0 score = 0.0 tags = set(scenic_tags.values_list("id", flat=True)) for tag_id, weight in user_weights.items(): total_weight += weight if tag_id in tags: score += weight return score / total_weight if total_weight else 0.04.3 把“一堆景点”变成“按天可执行的行程”
筛选出Top N个景点之后,怎么把它们排进“第1天、第2天、第3天”是个更麻烦的问题。我用的办法是一个“按天贪心分组+地理位置约束”的流程:
- 先把候选景点按评分降序排列。
- 第1天不排太满,因为用户第一天往往是从出发地到目的地,预留半天时间在路上。给第1天分配2个景点左右。
- 之后每一天,从剩余景点里取一个评分最高的作为锚点,再找距离锚点在5公里以内的其他高频景点,凑成一天3~4个。
- 同一天内的景点,按照建议游玩时长和历史行程数据排序,上午安排需要3小时以上时长的,下午安排2小时左右的,晚上安排餐饮类或者夜景类。
- 最后一天安排1~2个景点,因为用户需要返程。
这个逻辑对应的伪代码大致是这样:
def build_itinerary(candidates, days, rhythm): itinerary_days = [[] for _ in range(days)] available = sorted(candidates, key=lambda x: -x["score"]) day0_count = 1 if rhythm == "slow" else 2 for i in range(day0_count): itinerary_days[0].append(available.pop(0)) for d in range(1, days): if not available: break anchor = max(available, key=lambda x: x["score"]) available.remove(anchor) itinerary_days[d].append(anchor) near = [s for s in available if distance(anchor, s) < 5][:2] for s in near[:2]: itinerary_days[d].append(s) available.remove(s) return itinerary_days这只是一个简化版,真正写完还要处理“景点不足怎么办”“某个景点太热门但距离太远怎么取舍”等异常。但在答辩演示时,这个贪心策略已经能生成合理结果了。
4.4 用户反馈如何回到画像里
生成行程后,用户可能会手动调换景点顺序、删除某个景点,甚至把某个景点收藏了。这些行为不能白做,要把它接回到画像更新机制里:
- 用户收藏一个景点:将该景点所有标签的权重在user_tag表中加0.1。
- 用户替换/删除一个推荐景点:将与该景点相关的标签权重减0.1。
- 权重再乘以一个衰减系数,保证不会无限增长。
更新逻辑在Django服务里写成一个定时任务或请求触发任务都行。这个闭环很重要——有了反馈循环,系统才算得上“个性化”,而不是一次性推完就结束。
5. 联调、调试与毕设交付中的实际经验
前四章把架构和算法都讲清楚了,这一章聊点更“接地气”的。做这种带双技术栈的项目,代码写出来只是第一步,能不能顺利跑通、给老师演示,以及最终交付的文档能不能拿高分,往往决定了你做整个项目时最后的体感。
5.1 联调前必须先解决的三件事
如果SSM、Django、前端三端联调时出了问题,90%的原因会集中在这三个地方:
- 数据库字符集不统一:景点描述里全是中文,只要链接串没加UTF-8参数,查出来就是乱码。MySQL连接串一定要带
characterEncoding=utf8,建库时用utf8mb4。 - CORS跨域配置缺失:前端页面在SSM的8080端口,Django推荐服务在8000端口,只要涉及跨端口Ajax,就必须有CORS配置。SSM端也要写一个CORS过滤器,别只配一边。
- Token传参约定不一致:SSM签发JWT后,前端要把Token同时带给SSM和Django两端。如果SSM端用的请求头是
Authorization,Django端也必须是同一个名字,否则推荐服务永远提示“未登录”。
这些属于“哑巴坑”,不调试到那一步根本发现不了,但一旦出现,排查起来又非常明显。建议联调之前先把这三件事对照着检查一遍,能省下大半天时间。
5.2 造演示数据的艺术:别太空,也别太满
给老师演示的时候,最怕出现的情况是:景点库里只有5个景点,点“生成行程”两秒钟就出结果,一点惊喜感都没有。反过来,如果你导入5000条真实景点数据,生成算法处理起来很慢,演示的时候一直在转圈,同样翻车。
我的建议是,按“3个典型用户+2个热门城市+每个城市30个景点”来造数据。
- 3个典型用户分别代表三种画像:一个喜欢“自然风光+徒步”的年轻人,一个喜欢“美食探店+亲子友好”的家庭游客,一个“预算有限+历史古迹”的学生党。
- 每个城市准备30个景点,覆盖10个左右标签,保证用户偏好和景点标签有重叠,也有区分度。
- 给每个景点都配上真实的经纬度、门票、建议游玩时长和一张图片URL。
这样演示时,三个用户点进去会看到完全不同的行程,对比效果非常直观。老师问“个性化体现在哪”,直接切换两个用户,让他们看同一天、同一个城市的景点安排差异,就一目了然。
5.3 调试文档和讲解材料怎么组织
这个项目的交付物通常会包括源码、LW(论文/文档)、调试文档和讲解材料。作为有十多年经验的过来人,我提醒一句:文档写得规范,比多写100行代码更值钱。调试文档建议按这个结构组织:
- 环境说明:JDK版本、Python版本、MySQL版本、IDE版本、端口分配表。
- 数据库初始化说明:建库SQL、初始化数据SQL、默认账号密码。
- 接口测试方案:把SSM端的核心接口和Django端的推荐接口整理成Postman导出文件,标注每个接口的入参、出参示例、预期结果。
- 常见故障表:至少列10条运行中可能遇到的问题(端口冲突、中文乱码、跨域、Token过期、Django依赖缺失等),每条附排查步骤和解决方案。
讲解材料的核心是一个“按功能模块组织的演示脚本”:从登陆开始,每一步操作预期出现什么结果,你要讲什么话,都写清楚。不要到时候临场随机发挥,很容易因为一个接口报错而卡壳。
5.4 常见运行故障及处理一览
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| SSM启动后端口被占用 | 8080被其他程序占用 | 修改server.port,并同步前端代理端口 |
| Django接口报跨域错误 | CORS中间件未配置 | 添加CORS配置或接入django-cors-headers |
| 数据库中文乱码 | 连接串缺UTF-8参数 | 连接串加characterEncoding=utf8 |
| 前端调用推荐接口报401 | Token未传或参数名不一致 | 确认Header参数名统一为Authorization |
| Django报ModuleNotFoundError | requirements.txt未安装完整 | 在虚拟环境执行pip install -r requirements.txt |
| 行程生成结果为空 | 景点库数据不足或标签未匹配 | 检查scenic_tag数据是否完整、候选景点是否被预算过滤 |
调试过程中把这些问题全部记录到调试文档里,不仅是交付亮点,也能让你在最终答辩时更加从容。
5.5 源码和LW的交付注意事项
源码目录结构一定要清晰,建议这样组织:
project-root ├── ssm-server # SSM主业务服务 ├── django-service # Django推荐服务 ├── frontend # 前端页面(Vue/Thymeleaf等) ├── db # 建库SQL、初始化数据 └── docs # LW文档、调试文档、演示脚本LW文档(论文)的重点不用我多说,一定是“系统分析-系统设计-系统实现-系统测试”这条线。但我额外建议,在“系统测试”章节里,除了功能测试,一定要加一张“个性化效果对比”的表现:用三组不同偏好的用户,对同一城市生成行程,对比景点差异。这一页是整篇论文里最能体现“个性化”的实证材料,老师看到这一页通常会眼前一亮。
最后再多说几句
做这类系统,最大的感受是:不要被“个性化”三个字吓住,核心就那么两步——读懂用户想要什么,把景点合理地排成行程。Java+SSM和Django同时出现,不是为了炫技,而是让每个技术栈都留在自己最擅长的地方。整个项目做完,你会同时对Java服务端开发和Python数据处理都建立非常扎实的体感,这对后续找开发相关岗位或者继续做研究都有很直接的帮助。如果时间有余裕,还可以尝试把行程生成的结果导出成PDF攻略、接入地图API画线路图,或者加入一个基于物品协同过滤的相似景点推荐——但先把标签匹配这套闭环做扎实,毕业设计就完全够稳了。