
最近有不少准备做毕设的同学来问同一个题目“基于JavaSSMDjango的个性化旅游攻略定制系统”。说实话第一次看到这个标题我就知道这活儿不简单——它同时把两套后端技术栈塞进了一个项目里而且核心功能是“个性化推荐”不是单纯做个增删改查的旅游网站。这篇文章我就把自己在这个项目上的完整落地过程梳理一遍包括需求怎么拆、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。那为什么这个项目标题里要同时出现JavaSSM和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_rhythmtag标签表id, name, categoryuser_tag用户偏好标签关联表id, user_id, tag_id, weightscenic景点表id, name, description, address, lng, lat, ticket_price, duration_hours, cover_img, statusscenic_tag景点标签关联表id, scenic_id, tag_idarticle攻略文章表id, title, author_id, content, scenic_ids, create_timeitinerary行程主表id, user_id, title, days, total_budget, status, create_timeitinerary_day行程明细表id, itinerary_id, day_number, scenic_id, start_time, duration_hours, transport_tip, notefavorite收藏表id, user_id, scenic_id, create_timecomment评论表id, user_id, scenic_id, content, create_time, reply_id3.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, flatTrue)) 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, keylambda 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, keylambda 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连接串一定要带characterEncodingutf8建库时用utf8mb4。CORS跨域配置缺失前端页面在SSM的8080端口Django推荐服务在8000端口只要涉及跨端口Ajax就必须有CORS配置。SSM端也要写一个CORS过滤器别只配一边。Token传参约定不一致SSM签发JWT后前端要把Token同时带给SSM和Django两端。如果SSM端用的请求头是AuthorizationDjango端也必须是同一个名字否则推荐服务永远提示“未登录”。这些属于“哑巴坑”不调试到那一步根本发现不了但一旦出现排查起来又非常明显。建议联调之前先把这三件事对照着检查一遍能省下大半天时间。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参数连接串加characterEncodingutf8前端调用推荐接口报401Token未传或参数名不一致确认Header参数名统一为AuthorizationDjango报ModuleNotFoundErrorrequirements.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文档论文的重点不用我多说一定是“系统分析-系统设计-系统实现-系统测试”这条线。但我额外建议在“系统测试”章节里除了功能测试一定要加一张“个性化效果对比”的表现用三组不同偏好的用户对同一城市生成行程对比景点差异。这一页是整篇论文里最能体现“个性化”的实证材料老师看到这一页通常会眼前一亮。最后再多说几句做这类系统最大的感受是不要被“个性化”三个字吓住核心就那么两步——读懂用户想要什么把景点合理地排成行程。JavaSSM和Django同时出现不是为了炫技而是让每个技术栈都留在自己最擅长的地方。整个项目做完你会同时对Java服务端开发和Python数据处理都建立非常扎实的体感这对后续找开发相关岗位或者继续做研究都有很直接的帮助。如果时间有余裕还可以尝试把行程生成的结果导出成PDF攻略、接入地图API画线路图或者加入一个基于物品协同过滤的相似景点推荐——但先把标签匹配这套闭环做扎实毕业设计就完全够稳了。