尧图精选

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

🕒 发布时间:2026/10/1 4:50:57 📁 来源:尧图网络
前几天整理毕业设计资料翻到一个做到一半的QQ村旅游网站项目。这名字听着有点乡土味实际上就是一个功能完整的旅游门户网站景点介绍、旅游线路、攻略发布、地图展示、酒店预订几个核心板块一个不少。前端用的Bootstrap加JSP主后端是Java系的SSM框架也就是Spring、SpringMVC、MyBatis三件套另外还挂一个Python Flask服务专门处理推荐、文本检索和地图坐标这类轻量算法活。很多同学一看到JavaSSMFlask这种混合技术栈就心里发怵觉得是不是题目故意设计得复杂。其实这个组合拆开看特别合理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 iddeduceInventory UPDATE hotel SET inventory inventory - 1 WHERE id #{hotelId} AND inventory 0 /updateService层检测返回值等于0就抛一个业务异常回滚订单插入。整个方法加上Transactional保证扣库存和插订单要么都成功要么都失败。这是我这个项目里自认为写得最规范的一段逻辑答辩的时候也重点讲了这里。4. Flask辅助服务推荐、地图与攻略检索4.1 Flask服务怎么和Java通信Flask服务在这个项目里不是替代SSM而是当一个小工具人。它单独跑在5000端口Java后端通过HttpClient或者RestTemplate去调用。举个例子用户在景点详情页看到相似景点推荐前端请求Java的ControllerJava再转发请求给FlaskFlask侧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(host0.0.0.0, port5000)Java侧RestTemplate restTemplate new RestTemplate(); String url http://localhost:5000/api/recommend?scenic_id id; JSONObject json restTemplate.getForObject(url, JSONObject.class); ListScenicVO 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/recommendSSM那边在景点详情页异步调一次把返回的景点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默认8080Flask使用5000。要提前检查这两个端口没被占用别一启动就报Port already in use。字符集web.xml里必须配置SpringMVC的CharacterEncodingFilter否则中文会乱码。跨域虽然Java转发给Flask不需要跨域但如果你自己用Vue这类前端调试记得在Java端加CrossOrigin或配置Filter。5.2 部署到云服务器的流程部署的时候我踩过一个非常无语的坑以为一个war包就万事大吉结果发现Flask服务还要单独起。所以部署顺序应该是把Java项目用Maven打成war包丢到Tomcat的webapps目录。在服务器上安装Python3和pip把Flask的代码拉到某个固定目录用pip install -r requirements.txt装依赖。用nohup python app.py 让Flask服务在后台跑。配置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连接串后面加上serverTimezoneAsia/Shanghai。阿里云轻量服务器内存只有1GTomcat加Flask一起跑会比较吃紧需要调低JVM堆内存。在catalina.sh里设置JAVA_OPTS-Xms256m -Xmx512m。Flask服务的日志没有重定向nohup输出全丢了出问题根本不知道为啥。后来改成nohup python app.py flask.log 21 至少能看日志排查。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传JSONJava实体的字段驼峰命名如scenicNamePython字典习惯下划线如scenic_name导致数据映射不上。解决办法是Java那边给实体字段加JsonProperty(scenic_name)或者干脆让Flask返回时就统一用驼峰风格。问题三下单库存扣减为负数。检查更新语句有没有带AND inventory 0这个条件很多人只写inventory - 1没有并发保护。这个条件在SQL里加上后受影响行数才可靠。问题四中文乱码分两种情况。一种是网页显示乱码查JSP页面charset和SpringMVC过滤器一种是数据库里的中文变成问号检查数据库表字符集是否为utf8mb4连接串加useUnicodetruecharacterEncodingutf8。6.3 我的独家避坑技巧先跑通最小闭环我踩过最大的坑就是上来就全功能开发结果到处都是bug根本不知道是哪一层出的问题。后来我总结了一个顺序先做登录然后做景点列表和详情确保Java从数据库到前端是通的再做Flask的一个推荐接口用Postman测通再连调Java调用Flask最后才做攻略发布和酒店下单。每加一个功能就在已有闭环上扩充排查范围小信心也足。这个思维方式比任何框架知识都值钱。7. 最后分享一点个人体会这个项目做完我对技术栈选型这件事有了完全不一样的理解。以前总觉得一个项目应该尽量用同一种语言搞到后面发现真不是这么回事。SSM和Flask的混合组合相当于一个团队里既有严谨的财务又有灵活的设计师各干各的中间靠一份清晰的接口合同对接。对于以后工作也有启发任何架构都不是越统一越好而是越合适越好。另外我想给所有准备做类似课程设计或者毕业设计的同学一个建议不要让源码变成你唯一的救命稻草重点是能讲清楚每个模块为什么这么设计。比如为什么库存用乐观锁为什么Flask不直接连数据库为什么评论不分表这些我在上面的内容里都写了原因。答辩的时候老师看的就是你有没有思考过这些为什么。真把这些点吃透了哪怕代码是你自己一句一句敲的也完全值得理直气壮地讲给别人听。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →