news 2026/10/1 4:50:51

SSM+Flask混合架构旅游网站开发:从数据库设计到部署避坑实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM+Flask混合架构旅游网站开发:从数据库设计到部署避坑实践

前几天整理毕业设计资料,翻到一个做到一半的QQ村旅游网站项目。这名字听着有点乡土味,实际上就是一个功能完整的旅游门户网站,景点介绍、旅游线路、攻略发布、地图展示、酒店预订几个核心板块一个不少。前端用的Bootstrap加JSP,主后端是Java系的SSM框架,也就是Spring、SpringMVC、MyBatis三件套,另外还挂一个Python Flask服务专门处理推荐、文本检索和地图坐标这类轻量算法活。很多同学一看到Java+SSM+Flask这种混合技术栈就心里发怵,觉得是不是题目故意设计得复杂。其实这个组合拆开看特别合理,SSM负责业务逻辑强、事务要求高的部分,Flask负责灵活、计算密集、数据结构不固定的部分,两边用HTTP接口解耦,各干各的,是中小型项目里很实用的一套搭档。这篇文章我就把这个项目的技术拆解、数据库设计、核心模块实现、两套服务联调以及部署时容易踩的坑,全部摊开来讲清楚,打算做旅游类网站或者课程设计选这个方向的同学,可以直接拿来当参考。

1. 项目拆解:一个旅游网站为什么需要两套后端

1.1 技术栈选型的背后逻辑

先说说为什么这个项目要同时用SSM和Flask。很多人第一反应是"老师是不是故意为难我",真不是。SSM在Java生态里是经典的企业级框架组,Spring管对象装配和事务,SpringMVC接收请求、跳转页面,MyBatis把数据库表和Java对象映射起来,三者配合非常默契,适合处理用户、景点、线路、酒店、订单这类强结构、强约束的数据。登录要校验、下单要扣库存、评论要关联用户,这些场景都需要强事务和明确的类型检查,用Java做最稳妥。

但旅游网站里还有很多"非典型"功能,比如根据用户搜索词推荐相似景点,比如从大量攻略文本里提取关键词,再比如把地理位置转成地图上的坐标点。这些功能数据格式灵活、涉及字符串处理和数值计算,用Java写不是不能写,只是代码量会明显变大,调试周期长。而Python的Flask框架极其轻量,一个脚本就能起一个服务,配合分词库和数学库做文本相似度、坐标计算特别顺手。所以这个项目的思路是:SSM做主业务后端,Flask做算法与数据服务端,两者通过RESTful接口通信,Java这边用HTTP请求调用Python的接口,拿到JSON结果再组装给前端。这样各取所长,而且两边都可以独立开发、独立测试,互不拖泥带水。

1.2 模块划分与“QQ村”景点的真实场景代入

拿到项目题目时先别急着写代码,先把"QQ村"这个虚拟目的地需要哪些能力列清楚。我按照一个游客从出发前到游玩中的思路梳理:

  • 景点介绍:游客第一眼要看的就是有什么好玩的,所以景点模块要包含图片轮播、门票价格、开放时间、详细图文介绍和游客评论。
  • 旅游线路:很多游客不喜欢自己规划,直接买打包线路。线路模块要有几日游套餐、每日行程安排、人均价格。
  • 旅游攻略:提供用户发布攻略的入口,类似轻量版游记博客,需要登录后发布、后台审核、按热度排序。
  • 旅游地图:地图模块给每个景点提供位置标记,前端展示地图卡片,点击后显示路线距离。
  • 旅游特色:这部分是景区文化包装,比如特色美食、特产、民俗活动,本质是内容展示。
  • 酒店预订:围绕住的需求,提供酒店列表、房型价格、在线下单。

整个功能矩阵覆盖了"看景点、选线路、查攻略、看地图、买特色、订酒店"的完整闭环。SSM负责除了推荐和地图计算之外的所有业务,Flask负责"相似景点推荐"和"地图经纬度生成"这两个相对独立的服务。这样划分,某一个服务挂了不至于整个网站瘫掉,也方便多人协作开发,一个人负责Java端一个人负责Python端,只要提前约定好接口文档就行。

2. 数据库设计与核心表结构

2.1 建表思路:围绕“游前-游中-游后”铺开

数据库设计是这类项目的根基,表建不合理后面全是坑。QQ村旅游网站我用的MySQL,建表时按业务域分了几组:

用户域:一张user表,字段包括id, username, password, nickname, phone, avatar, create_time。密码不存明文,用MD5加盐处理,这个后面单独说。

内容域:scenic景点表、line线路表、strategy攻略表、hotel酒店表。以景点表为例:

CREATE TABLE `scenic` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `description` TEXT, `cover_image` VARCHAR(255), `tp_price` DECIMAL(10,2), `address` VARCHAR(255), `latitude` DECIMAL(10,6), `longitude` DECIMAL(10,6), `open_time` VARCHAR(50), `status` TINYINT DEFAULT 1 );

这里故意把经纬度字段放在景点表里,目的就是配合Flask的地图服务。Flask拿到景点地址字符串可以调用第三方或者本地维护一个坐标表返回经纬度,Java端插入数据时调用一次,之后直接展示,不用重复计算。

交易域:hotel_order酒店订单表,字段有id, user_id, hotel_id, room_type, check_in_date, check_out_date, order_status, create_time。这里特别要注意加一个version字段,后面说防超卖要用。

互动域:comment评论表和strategy攻略表关联用户和景点,评论表设计成多态关联也可以,但为了查询方便,我直接用单独的scenic_comment和strategy_comment,不要偷懒搞一个万能评论表,查询时各种类型判断麻烦得不行。

2.2 一个扩展性设计:用类型字段处理“特色”内容

旅游特色模块有点特殊,它里面既有文字介绍又有美食图片,还可能带视频链接。直接为它单独建好几张表没必要,我用一张content表解决:

CREATE TABLE `content` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `type` TINYINT COMMENT '1美食,2特产,3民俗', `title` VARCHAR(100), `content_text` TEXT, `image_url` VARCHAR(255), `sort_order` INT DEFAULT 0 );

页面侧边栏和主页展示时,只需要根据type字段做条件查询,按sort_order排序就行。这样以后要加"节庆活动"这类新类型,不用改表结构,只加一个类型编号,扩展成本极小。其他的攻略分类、线路标签也可以沿用这种"字段枚举"思路,别一上来就搞几十张表,维护起来头大。

2.3 索引与外键:够用就好

学生项目容易走两个极端,要么一个索引都不加,要么外键一大堆导致插入删除巨慢。我的做法是:外键逻辑上用,数据库层面不强加物理外键。比如hotel_order.user_id代表归属用户,但建表时不写FOREIGN KEY,靠Service层去保证引用不出错。这样既能减少死锁概率,又方便后期分库分表。索引方面,用户表的username建唯一索引,景点表的name建普通索引,订单表的user_id建普通索引,覆盖了最常用的登录、搜索、用户订单列表三个高频查询就行。

3. SSM主服务:从登录鉴权到预订下单

3.1 经典三层架构落地

SSM项目的目录结构我习惯按模块分包,而不是按技术层分。比如controller/user、controller/scenic,对应的Service接口放service包,Mapper接口放mapper包,XML文件放resources/mapper。这样每个功能模块的所有相关类都在一起,出问题能顺着包名快速定位。

每个模块的流转都长得差不多:Controller接收参数 -> 装成VO或DTO -> 调Service接口 -> Service实现里调Mapper -> MySQL返回结果 -> Service组装逻辑 -> Controller返回JSON或者跳转页面。以景点详情为例:

@Controller @RequestMapping("/scenic") public class ScenicController { @Autowired private ScenicService scenicService; @RequestMapping("/detail/{id}") public String detail(@PathVariable Integer id, Model model) { Scenic scenic = scenicService.getById(id); model.addAttribute("scenic", scenic); return "scenic/detail"; } }

注意这里返回的是String,配合SpringMVC的视图解析器跳转到JSP页面。如果做前后端分离,就改成@ResponseBody返回一个Result对象,但课程设计里用JSP模板渲染其实更直观,也更容易给答辩老师讲清楚。

3.2 登录与权限:拦截器和Session千万别漏

登录模块是这类网站的标配。我用SpringMVC的拦截器实现"必须登录才能发布攻略、预订酒店"的权限控制。写一个LoginInterceptor,在preHandle里检查session.getAttribute("loginUser"),为空就重定向到登录页。

密码加密这里要重点提一句:直接明文存密码,答辩时老师一句话就能问倒你。我用的是MD5加盐,注册时生成一个随机盐值拼在密码后面再取MD5,数据库里同时存盐和加密结果。校验时再把盐拿出来拼一遍算一次MD5比对。虽然MD5不算最安全,但课程设计要求里足够用了,至少比明文强一百倍。

public class Md5Utils { public static String encrypt(String password, String salt) { String base = password + salt; return DigestUtils.md5DigestAsHex(base.getBytes(StandardCharsets.UTF_8)); } }

3.3 预订下单:用乐观锁防止库存超卖

酒店预订这个模块是个绝佳的答辩亮点。很多学生项目就是简单insert一条订单完事,根本不考虑库存。我加了一个inventory字段到酒店房型表,下单时先查库存,大于0才扣减并插入订单。这里最关键的是"先查后扣"并不是线程安全的,两个用户同时下单时可能都看到库存为1,然后一起扣成0,售出两间房。解决方式有两种:

最稳妥是乐观锁,在更新语句里带上版本号条件:

UPDATE hotel SET inventory = inventory - 1 WHERE id = ? AND inventory > 0;

用MyBatis执行这个Update,然后判断受影响行数,如果是0说明抢不到库存,直接返回"房间已满"。MyBatis的XML里这么写:

<update id="deduceInventory"> UPDATE hotel SET inventory = inventory - 1 WHERE id = #{hotelId} AND inventory > 0 </update>

Service层检测返回值,等于0就抛一个业务异常,回滚订单插入。整个方法加上@Transactional,保证扣库存和插订单要么都成功要么都失败。这是我这个项目里自认为写得最规范的一段逻辑,答辩的时候也重点讲了这里。

4. Flask辅助服务:推荐、地图与攻略检索

4.1 Flask服务怎么和Java通信

Flask服务在这个项目里不是替代SSM,而是当一个小工具人。它单独跑在5000端口,Java后端通过HttpClient或者RestTemplate去调用。举个例子,用户在景点详情页看到"相似景点推荐",前端请求Java的Controller,Java再转发请求给Flask:

Flask侧:

from flask import Flask, request, jsonify app = Flask(__name__) @app.route('/api/recommend', methods=['GET']) def recommend(): scenic_id = request.args.get('scenic_id') # 这里用简单的基于关键词的相似度算法计算 result = compute_similar(scenic_id) return jsonify({'code': 200, 'data': result}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

Java侧:

RestTemplate restTemplate = new RestTemplate(); String url = "http://localhost:5000/api/recommend?scenic_id=" + id; JSONObject json = restTemplate.getForObject(url, JSONObject.class); List<ScenicVO> recommendList = json.getJSONArray("data");

联调时最要注意的是跨域问题,如果前端直接调Flask服务,需要给Flask加上flask-cors。但在这个项目里前端只和Java通信,Java充当网关,所以没有跨域烦恼,这也算是混合架构的一个隐性好处。

4.2 基于关键词的景点推荐简单实现

景点推荐不用搞什么高大上的协同过滤,靠景点名称与描述的文本相似度就够了。我用到了jieba分词和numpy计算余弦相似度。思路是:把所有景点的名称、描述、标签拼接成一个文本,用户当前浏览的景点也拼一个文本,然后分词、去停用词、构造词频向量,再算两个向量夹角的余弦值。余弦值越接近1说明越相似。

import jieba import numpy as np from collections import Counter def tokenize(text): return [w for w in jieba.cut(text) if w.strip() and w not in stopwords] def cosine_similarity(vec1, vec2): all_words = set(vec1) | set(vec2) v1 = Counter(vec1) v2 = Counter(vec2) arr1 = np.array([v1.get(w, 0) for w in all_words]) arr2 = np.array([v2.get(w, 0) for w in all_words]) return np.dot(arr1, arr2) / (np.linalg.norm(arr1) * np.linalg.norm(arr2) + 1e-8)

实际跑下来效果还可以,"溶洞奇观"会推荐给"地下暗河","山居客栈"会推荐给"农庄度假",对于毕业设计来说完全够用。这个接口写成/api/recommend,SSM那边在景点详情页异步调一次,把返回的景点id列表拿去数据库里查完整信息再渲染。整个过程解耦清晰,Java管查询,Python管算数。

4.3 地图坐标接口:让前端少写逻辑

旅游地图模块如果直接用第三方地图SDK,需要在前端塞一堆JS逻辑,而且第三方平台需要申请密钥,在课程设计里容易出岔子。我的做法是:数据库里已经存了景点经纬度,Flask这边提供一个接口,把经纬度转换成前端地图插件需要的数据格式,比如高德地图的坐标格式。

@app.route('/api/map/points', methods=['GET']) def map_points(): scenic_list = load_scenic_from_db() points = [] for s in scenic_list: points.append({ 'name': s['name'], 'longitude': s['longitude'], 'latitude': s['latitude'], 'info': s['description'][:50] }) return jsonify(points)

前端用百度地图或者高德地图的JS库加载这些点就行。这里有一个经验:不要在Flask里直接用pymysql操作数据库和Java共用的库,容易导致连接池混乱。我是让Java那边提供一个接口来导出景点基础数据,Flask启动时拉一次放在内存里,或者定时刷新。这样两个服务不直接争抢数据库连接,线上稳定很多。

5. 联调与部署:先本地后服务器

5.1 本地联调需要改哪些配置

我把整个项目跑通本地,花时间最多的不是写代码,而是连调各种配置。列一个清单:

  • 数据库连接:Java的jdbc.properties里要改用户名密码,MySQL要允许远程连接否则连不上。Flask读取Java导出的数据时不需要直接连数据库,所以我没让Flask配置数据库。
  • 端口规划:Tomcat默认8080,Flask使用5000。要提前检查这两个端口没被占用,别一启动就报Port already in use。
  • 字符集:web.xml里必须配置SpringMVC的CharacterEncodingFilter,否则中文会乱码。
  • 跨域:虽然Java转发给Flask不需要跨域,但如果你自己用Vue这类前端调试,记得在Java端加@CrossOrigin或配置Filter。

5.2 部署到云服务器的流程

部署的时候我踩过一个非常无语的坑:以为一个war包就万事大吉,结果发现Flask服务还要单独起。所以部署顺序应该是:

  1. 把Java项目用Maven打成war包,丢到Tomcat的webapps目录。
  2. 在服务器上安装Python3和pip,把Flask的代码拉到某个固定目录,用pip install -r requirements.txt装依赖。
  3. 用nohup python app.py &让Flask服务在后台跑。
  4. 配置Nginx反向代理,域名根路径转发到Tomcat8080,/api/flask/前缀的路径转发到Flask5000端口。

Nginx配置片段:

location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location /flask/ { proxy_pass http://127.0.0.1:5000/; proxy_set_header Host $host; }

这里proxy_pass http://127.0.0.1:5000/;后面带个斜杠,会把/flask/api/recommend转发成Flask的/api/recommend,是很多新手容易忽略的细节。

5.3 服务器环境的坑:时区、内存与日志

部署到服务器后,我遇到三个很典型的问题:

  • MySQL的serverTimezone没设置成Asia/Shanghai,导致Java连接时报错或者时间差8小时。解决方式是JDBC连接串后面加上serverTimezone=Asia/Shanghai。
  • 阿里云轻量服务器内存只有1G,Tomcat加Flask一起跑会比较吃紧,需要调低JVM堆内存。在catalina.sh里设置JAVA_OPTS="-Xms256m -Xmx512m"。
  • Flask服务的日志没有重定向,nohup输出全丢了,出问题根本不知道为啥。后来改成nohup python app.py > flask.log 2>&1 &,至少能看日志排查。

6. 常见问题排查手册

6.1 环境类问题

问题一:Tomcat能启动,但访问页面404。

先看控制台有没有报错,最常见是ClassNotFound,说明jar包没装全。IDEA里检查Artifacts有没有把依赖的jar合入,或者用Maven执行package重新打war包。

问题二:MySQL连接不上,报Access denied for user。

确认jdbc.properties里的用户名密码是否有权限,用命令行先试一下能不能连上。注意千万别用root账号但密码里带了#或&这种特殊字符,在配置里会被截断。

问题三:Flask服务启动报端口占用。

Mac或Linux用lsof -i:5000查看占用进程,Windows用netstat -ano | findstr 5000,查到PID后杀掉即可。

6.2 代码逻辑类问题

问题一:MyBatis绑定异常,提示Invalid bound statement (not found)。

通常是Mapper接口和XML文件没在同一个包路径下,或者XML的namespace写错了。检查mybatis-config.xml里的mapper-locations是否配置为classpath:mapper/*.xml。

问题二:JSON互相转换时字段丢失。

Java和Flask之间通过HTTP传JSON,Java实体的字段驼峰命名如scenicName,Python字典习惯下划线如scenic_name,导致数据映射不上。解决办法是Java那边给实体字段加@JsonProperty("scenic_name"),或者干脆让Flask返回时就统一用驼峰风格。

问题三:下单库存扣减为负数。

检查更新语句有没有带AND inventory > 0这个条件,很多人只写inventory - 1,没有并发保护。这个条件在SQL里加上后,受影响行数才可靠。

问题四:中文乱码分两种情况。

一种是网页显示乱码,查JSP页面charset和SpringMVC过滤器;一种是数据库里的中文变成问号,检查数据库表字符集是否为utf8mb4,连接串加useUnicode=true&characterEncoding=utf8。

6.3 我的独家避坑技巧:先跑通最小闭环

我踩过最大的坑就是上来就全功能开发,结果到处都是bug,根本不知道是哪一层出的问题。后来我总结了一个顺序:先做登录,然后做景点列表和详情,确保Java从数据库到前端是通的;再做Flask的一个推荐接口,用Postman测通;再连调Java调用Flask;最后才做攻略发布和酒店下单。每加一个功能,就在已有闭环上扩充,排查范围小,信心也足。这个思维方式,比任何框架知识都值钱。

7. 最后分享一点个人体会

这个项目做完,我对"技术栈选型"这件事有了完全不一样的理解。以前总觉得一个项目应该尽量用同一种语言,搞到后面发现真不是这么回事。SSM和Flask的混合组合,相当于一个团队里既有严谨的财务,又有灵活的设计师,各干各的,中间靠一份清晰的接口合同对接。对于以后工作也有启发,任何架构都不是越统一越好,而是越合适越好。

另外我想给所有准备做类似课程设计或者毕业设计的同学一个建议:不要让"源码"变成你唯一的救命稻草,重点是能讲清楚每个模块为什么这么设计。比如为什么库存用乐观锁,为什么Flask不直接连数据库,为什么评论不分表,这些我在上面的内容里都写了原因。答辩的时候老师看的就是你有没有思考过这些"为什么"。真把这些点吃透了,哪怕代码是你自己一句一句敲的,也完全值得理直气壮地讲给别人听。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 4:50:45

AI-CAD工程落地:CLI驱动的FreeCAD自动化实践

1. 这不是技术不行&#xff0c;是工程逻辑没对齐“AI CAD”这四个字在2024年几乎成了工业软件圈的流量密码。你刷技术社区、看行业展会、翻融资新闻&#xff0c;满屏都是“AI驱动智能设计”“大模型自动生成DXF”“FreeCAD接入LLM实现语义建模”——演示视频做得比电影还丝滑&…

作者头像 李华
网站建设 2026/10/1 4:50:06

CAP定理在时序数据库中的扭曲表现与优化实践

很多人第一次接触CAP定理是在分布式系统教科书上&#xff0c;讲的是Consistency、Availability、Partition Tolerance三者不可兼得。做业务数据库的人通常记住一句话&#xff1a;网络分区发生时&#xff0c;要么保一致性&#xff0c;要么保可用性。这套框架用在传统OLTP系统上问…

作者头像 李华
网站建设 2026/10/1 4:49:33

AI落地三大隐形成本:数据清洗、模型监控与人机协作

1. 这份报告不是“抄来的PPT”&#xff0c;而是我蹲在一线三年攒出来的趋势手记“AI发展趋势调研报告”这八个字&#xff0c;现在几乎成了所有行业会议、立项材料、融资BP里必塞的标配模块。但说实话&#xff0c;我见过太多所谓“报告”——要么是把Gartner曲线截图放大三倍配个…

作者头像 李华
网站建设 2026/10/1 4:49:33

从埃氏筛到线性筛:质数筛法的原理、优化与工程实践

1. "判断质数"和"筛质数"是两码事&#xff1a;从 O(√n) 到 O(n)1.1 单个数判断的最朴素养子我第一次接触质数筛法&#xff0c;是在想通一个问题之后&#xff1a;筛到底是什么意思&#xff1f;很多初学者&#xff08;包括当年的我&#xff09;刚上手时&…

作者头像 李华
网站建设 2026/10/1 4:49:33

AI落地实战指南:小模型、可插拔服务与数据活化

1. 这份报告不是“抄来的PPT”&#xff0c;而是我蹲在产线、泡在实验室、跟37个AI团队聊完后攒出来的真东西“AI发展趋势调研报告”这八个字&#xff0c;现在满大街都在用——招聘JD里写“需熟悉AI发展趋势”&#xff0c;投资人尽调清单第一条是“请提供贵司对AI发展趋势的理解…

作者头像 李华
网站建设 2026/10/1 4:48:59

基于SpringBoot+Vue+MyBatis+MySQL的服装生产管理系统

前阵子整理了一套基于SpringBootVue的服装生产管理系统源码&#xff0c;春节前刚好利用这套骨架给一家做针织衫的中小型服装厂做过信息化改造摸底。整套系统跑下来&#xff0c;我觉得它特别适合两类人&#xff1a;一类是刚入行Java后端、想找一个能完整跑通的实战项目练手的朋友…

作者头像 李华