线上旅游体验系统这个题目这几年在不少课程设计和毕业设计里都能看到。我最近刚好完整实现了一个基于JavaSSMFlask的版本从数据库设计到后端接口再到前端页面全部自己搭前后折腾了大概三周。简单说这套系统把景点展示、线路规划、虚拟体验、在线预订、订单管理这些功能串在一起用户可以像逛旅行平台一样浏览目的地、看全景素材、收藏线路然后直接下单。对正在做相关项目的同学来说这套组合尤其值得参考SSM负责核心业务Flask负责推荐和虚拟体验接口两种技术栈在一个项目里协同工作既能体现Java后端的扎实又能展现Python生态的灵活。1. 项目整体思路与功能拆解1.1 为什么做线上旅游体验系统线上旅游体验系统解决的问题很实在用户在下单前往往只能通过几张图片和一段介绍来判断目的地决策成本高实际到了现场又容易“图片与实物不符”。把旅游体验搬到线上本质上是给用户提供一个更加立体的“前置感知层”。我在设计时把整个链路拆成“决策-体验-预订-服务”四个环节。决策环节靠景点检索和线路推荐体验环节靠全景素材和短视频预订环节靠标准的订单流程服务环节靠评论和售后状态管理。这四个环节不是孤立的而是共享同一套用户账号体系和数据库。比如用户看了某个景点的虚拟体验视频并标记“想去”系统就会记录这个行为标签后续做线路推荐时优先展示相似主题的线路。这次项目的核心目标并不是做一个纯展示类的旅游信息网站而是做成一个具备交易闭环的业务系统。所以功能上既要有内容展示也要有下单和订单管理管理员要有审核和统计后台。整体看下来这套系统可以覆盖旅游平台最基础的业务形态后续做商业项目也能直接迁移。1.2 功能模块全景用户端和管理端的功能划分我建议在动手前就写清楚不要边做边加。用户端核心功能包括注册登录手机号或用户名注册登录后进入个人中心。景点浏览与检索按城市、分类、关键词、价格区间筛选景点。线路推荐首页展示热门线路推荐接口根据用户兴趣动态调整。虚拟体验景点关联全景图片、短视频、语音讲解用户可在线体验。收藏管理收藏景点或线路方便后续查看。在线预订选择线路、填写联系人信息、选择出游人数模拟支付。订单管理查看订单状态取消未支付订单确认收货或申请退款。评论评分下单并完成行程后对景点和线路进行评价。管理端核心功能包括景点管理新增、编辑、上下架景点维护图片视频素材。线路管理组合景点、设置行程天数与价格、调整推荐权重。订单管理查看所有订单处理退款申请调整订单状态。用户管理启用或禁用账号查看用户行为记录。数据统计统计景点浏览量、订单量、热门目的地排行。这个模块清单看起来不少但实际落地时每块都不算特别复杂。关键是表结构要先设计好否则做到后面容易返工。1.3 为什么选SSMFlask双后端标题里有JavaSSM又有Flask这不是为了堆技术栈而是有实际分工考虑的。SSM最适合写强业务逻辑用户、订单、库存、支付状态机这些都需要明确的事务边界和类型约束。Spring管理Bean和事务SpringMVC处理路由分发MyBatis和数据库打交道每个层次职责都很清楚排查问题相对容易。Flask则适合做灵活度高的辅助服务。比如我的推荐模块需要读取用户行为数据计算标签相似度输出候选线路。用Python写一个简单的协同过滤代码量只有Java版本的三分之一左右。虚拟体验模块也需要频繁处理JSON格式的体验日志Python处理字典和JSON非常顺手。所以最终架构就定成“主业务在SSM推荐和体验服务在Flask”。两个后端之间通过HTTP接口通信SSM是主入口Flask相当于一个独立微服务。这样的结构还有一个好处以后想换推荐算法、或者把Flask替换成其他Python服务都不影响主流程。2. 技术栈与核心架构解析2.1 SSM端的组成与分工SSM可以说是JavaWeb领域最经典的组合之一。Spring负责把业务对象纳入容器管理同时提供声明式事务SpringMVC接收浏览器请求通过HandlerMapping找到对应的ControllerMyBatis把Mapper接口和SQL语句映射起来返回实体对象。我在实际编码时会把代码分成四个层次Controller只做参数接收和结果包装Service放业务判断和事务控制Mapper做数据库操作Entity对应表结构。比如下单这个操作Controller接收userId和lineIdService里面依次做参数校验、线路库存检查、生成订单号、插入订单记录、扣减库存任何一步失败整个事务回滚。Spring和SpringMVC的配置我习惯用JavaConfig和XML混合的方式。Spring管理数据源、事务管理器、Mapper扫描SpringMVC管理控制器、视图解析器、静态资源。其实项目中前后端以接口为主视图解析器用得很少返回体统一用Result对象包装前端拿到code和data再处理。2.2 Flask端扮演的角色Flask在这个项目里主要承担三件事。第一提供推荐接口。它从数据库读取用户最近的浏览和收藏记录计算与线路标签的相似度把TopN结果返回。第二提供虚拟体验素材聚合接口。景点关联了全景图、视频、音频讲解Flask读取配置后一次性返回给前端。第三接收体验日志比如用户看了多长时间、对哪些景点感兴趣这类数据写入体验日志表为推荐提供原始素材。下面是我在Flask端最核心的一个推荐接口代码非常简单from flask import Flask, request, jsonify import recommend app Flask(__name__) app.route(/api/recommend, methods[POST]) def recommend_lines(): data request.get_json() user_id data.get(userId, 0) result recommend.recommend_by_tags(user_id) return jsonify({code: 0, data: result})这段代码的作用就是把HTTP请求参数转换成用户ID然后调用纯Python实现的推荐函数返回JSON列表。它不关心订单事务也不管权限校验所以逻辑足够轻量。把这种非核心且变化频繁的逻辑放进FlaskSSM端就不会被算法代码拖累。2.3 前后端数据交互方式前端我采用Vue Element Plus开发管理端和用户端共用一套工程通过路由区分。开发环境有两个代理一个把/api转发到SSM的8080端口另一个把/flask转发到Flask的5000端口。// vite.config.js export default { server: { proxy: { /api: http://localhost:8080, /flask: http://localhost:5000 } } }为什么要在开发环境做代理而不是直接跨域请求因为直接访问不同端口会触发浏览器的CORS限制每次都要在Java端写跨域配置麻烦且不安全。开发时用代理生产时用Nginx统一转发可以让前后端域名保持一致。生产环境我建议在Nginx层做路由/api开头的请求转发给Java服务/flask开头的请求转发给gunicorn启动的Python服务静态资源由Nginx直接托管。这样对外只暴露一个入口浏览器不会发生跨域安全性也更好控制。3. 数据库设计与核心表结构3.1 用户与权限设计用户表是所有业务的基础我设计时会避免把角色字段直接存成user.role这种单字段模式。因为系统后续很可能出现普通用户、管理员、导游、商家等多种角色单字段扩展性太差。这次项目采用经典的多对多角色模型user表存用户基本信息role表存角色user_role表存关联关系。用户表核心字段包括id主键自增。username登录名唯一索引。password加密后的密码。salt加密盐值。nickname昵称。avatar头像地址。phone手机号。email邮箱。status账号状态1正常0禁用。create_time注册时间。密码字段必须使用加盐哈希不要存明文也不要只做一次简单MD5。用户可以注册成功但是密码字段绝对不能裸奔。我在项目里用SHA-256加随机盐虽然不算最强但作为课程项目足够示范安全思路。3.2 景点与线路数据模型景点表设计的关键点在于素材字段的规划。一个景点往往有主图、短视频、全景图、语音讲解等多个素材可以都放景点表里也可以单独拆素材表。规模不大时直接放在景点表更直观。CREATE TABLE spot ( id int NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL, city varchar(50) DEFAULT , category varchar(50) DEFAULT , summary text, cover_url varchar(255) DEFAULT , video_url varchar(255) DEFAULT , panorama_url varchar(255) DEFAULT , price decimal(10,2) DEFAULT 0, status int DEFAULT 1, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;线路表用于组合多个景点。最简单的做法是用一个spot_ids字段把景点ID用逗号拼起来比如“1,3,7”。这种设计适合演示项目查询时在Java端拆分成数组再逐条查景点信息。如果线路数量很大、景点关联关系复杂还是建议拆成line_spot中间表。3.3 订单与评论体系订单表是交易环节的核心状态字段最需要谨慎设计。我使用status表示订单状态整数定义状态机0待支付1已支付2已完成3已取消4已退款金额字段使用decimal(10,2)Java端对应BigDecimal不要用double或float。金额精度问题是很多项目里反复出现的典型坑。一次订单包含联系人、手机号、出游人数、总金额、下单时间、支付时间字段不多但中间状态一定要想清楚。评论表则关联用户、景点、订单三张表。同一个用户对同一个景点只能评论一次可以通过order_id和user_id建立唯一约束来实现。评分字段我设计为1到5的整数简单直观统计均分也方便。4. 核心模块实现要点4.1 登录鉴权与会话管理SSM端登录鉴权我没有引入Spring Security这种重框架而是用拦截器加Session的方案。用户登录成功后把用户ID和用户名放进Session同时更新最近登录时间。拦截器里写一个HandlerInterceptor对于需要登录的路径检查Session是否有用户信息没有就跳转到登录接口并返回未登录提示。简单Session方案在单机部署下完全够用。不过我还是建议在这一步加入JWT的思路登录成功后由后端签发一个token前端存到LocalStorage请求头里带Authorization字段。服务端通过过滤器解析token获取用户信息。这样以后做小程序或移动端接口时直接复用同一套鉴权逻辑。token里只放用户ID、昵称、过期时间这些必要信息千万别放密码。我见过有人直接把整个用户对象塞进token这是完全没有必要的风险。过期时间建议设置短一些比如30分钟用户操作时再续期安全性更高。4.2 景点检索与线路推荐景点检索接口要考虑多条件组合。城市、分类、关键词、价格区间都可能为空MyBatis里用动态SQL来实现select idsearchSpots resultTypeSpot select * from spot where if testcity ! null and city ! and city #{city} /if if testcategory ! null and category ! and category #{category} /if if testkeyword ! null and keyword ! and (name like concat(%, #{keyword}, %) or summary like concat(%, #{keyword}, %)) /if if testminPrice ! null and price gt; #{minPrice} /if if testmaxPrice ! null and price lt; #{maxPrice} /if /where order by create_time desc /select线路推荐我放在Flask端实现。思路是将景点的分类和标签转化为特征向量用户在浏览、收藏、体验过程中产生行为记录系统把这些行为映射成用户的兴趣向量再计算兴趣向量与线路向量的余弦相似度。没有行为数据的用户直接按照销量和评分排序做热门兜底保证接口一定有返回结果。第一次实现时不要贪多先做“相似度热门兜底”就够展示了。4.3 线上虚拟体验模块实现虚拟体验模块是这个项目最有辨识度的功能。我采用“全景图短视频语音讲解”三种素材配合的方式。用户进入景点详情页后可以观看全景图也可以直接播放景点宣传短视频。体验结束后前端会提交一条体验记录包含用户ID、景点ID、观看时长、是否点“想去”。Flask端接收体验记录后把数据写入experience_log表同时更新用户的兴趣标签权重。比如用户多次观看“古镇”类景点那么古镇标签的权重就会增加。这些数据后续喂给推荐接口就能让推荐结果越来越接近用户的真实偏好。虚拟体验模块的另一个作用是收集用户的决策信息哪个景点的视频完播率高哪个景点的全景图浏览时间长哪个景点的“想去”按钮点击次数多。这些统计指标对于管理员调整景点排序很有参考价值。所以千万不要把虚拟体验做成一个静态的视频播放页面它应该是整个推荐逻辑的数据入口。4.4 预订与支付流程由于项目不接入真实第三方支付我实现了模拟支付流程。用户选择线路后创建订单订单状态为“待支付”。在订单详情页点击“模拟支付”后端校验订单归属和状态后将状态改成“已支付”同时生成一条支付流水记录支付时间写入订单表。这个流程的关键在于事务控制。创建订单时要在一个事务里完成校验线路是否存在、校验库存、生成订单号、插入订单、扣减库存。任何一个环节失败都要整体回滚。最常出问题的是并发扣库存。线下场景里多个人同时下单很容易超卖。解决方案是扣减库存时使用UPDATE ... WHERE stock 0这样的条件更新而不是一条普通UPDATE。这样数据库行锁会保证同一时刻只有一个事务能成功扣减避免库存变成负数。5. 调试与部署中的常见问题5.1 环境版本冲突我这次使用的环境组合是JDK 8、Maven 3.6、Tomcat 9、MySQL 5.7、Python 3.8。这个组合非常稳定网上相关资料也最多不建议一上来就追求JDK 17和最新Tomcat容易遇到兼容性问题。Maven依赖里Lombok版本和JDK版本不兼容是最常见的坑。表现为编译时突然找不到getter/setter方法或者提示Unknown tag。解决办法是固定Spring Boot或Spring版本使用配套的Lombok版本。Flask这边依赖文件一定要用requirements.txt锁版本否则不同机器下载到不同版本行为可能完全不一样。5.2 跨域与接口联调前后端分开部署后跨域问题是一定要面对的。开发环境我使用Vite代理生产和测试环境使用Nginx。这样做的最大好处是浏览器看到的所有请求都指向同一个域名不会产生跨域问题。如果确实需要临时在Java端配置跨域可以使用一个简单的Filter在响应头里添加Access-Control-Allow-Origin: * Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, Authorization但要注意生产环境用*开放所有来源并不安全最好限定到自己的前端域名。更多时候我们其实不需要Java端开CORSNginx转发才是更干净的方案。5.3 静态资源与上传路径景点图片和视频的管理是很容易被忽略的部分。很多新手会把上传的文件保存到项目目录下比如webapp/uploads结果重启之后文件丢失或者部署到服务器后发现路径拿不到。正确做法是把上传目录配置成磁盘绝对路径比如Linux下的/data/tour/uploads然后通过SpringMVC的静态资源映射让URL能访问到这个目录。Windows和Linux的路径分隔符不一样不要硬编码\或者/。用File.separator或者Paths.get()拼接路径。我遇到过本地Windows测试正常放到Linux服务器上图片全部加载失败排查半天就是路径分隔符写死的问题。5.4 常见异常速查表异常现象可能原因解决方法数据库中文乱码连接URL缺少编码参数在JDBC URL后加characterEncodingutf8数据库和表也使用utf8mb4接口返回404请求路径与Controller不匹配检查RequestMapping路径和前端请求地址是否完全一致上传图片无法访问静态资源映射未配置SpringMVC中配置虚拟路径映射到上传磁盘目录跨域请求被拦截前端端口与后端不一致使用Nginx反向代理或Vite代理转发Flask接口连接失败服务未启动或端口占用检查app.run端口使用lsof或netstat查看下单库存变负数并发未控制扣库存使用条件更新UPDATE spot SET stockstock-1 WHERE stock0这张表我建议直接放进调试文档里。很多问题不是代码逻辑难而是环境问题反复出现记录下来能省很多时间。6. 调试文档与二次开发建议6.1 调试文档该怎么写调试文档的意义是让一个完全没有接触过这个项目的人能在一台新电脑上30分钟内把服务跑起来。不是简单写一句“先运行后端再运行前端”而是要把环境、步骤、配置和测试数据都写清楚。我习惯把调试文档分成五块运行环境版本、数据库初始化脚本、需要修改的配置文件、启动顺序、测试账号。其中启动顺序特别重要我见过有人不先启动MySQL就启动后端报错之后以为是代码问题。文档里写清楚“先启动MySQL再启动SSM再启动Flask最后启动前端”能避免大量无效沟通。测试账号至少准备两类一个普通用户一个管理员。普通用户能看景点、下单管理员能进后台管理。每个模块记录对应的接口调用方式和预期返回结果这个文档越细调试越轻松。6.2 源码结构讲解SSM端采用标准Maven结构controller存放接口层service存放业务逻辑mapper存放MyBatis接口和XMLentity存放数据库实体。工具类和常量放到util配置信息放到resources。Flask端的目录不需要太复杂我建议一个app.py负责路由和启动一个recommend.py负责推荐算法一个db_helper.py封装数据库连接和查询数据配置放config.py。不要把几百行代码堆进一个文件虽然Flask允许但后期维护会很痛苦。数据库连接信息建议用环境变量或外部配置文件方式读取不要硬编码在源码里。尤其是Flask端的配置源码一旦提交到仓库密码就泄露了。虽然课程项目可能不涉及公网部署但养成这种习惯对商业项目非常重要。6.3 后续扩展方向这套系统扩展性很好。比较实用的方向包括接入地图Web API把景点位置标注在地图上用户浏览时可以看到线路的地理走向加入拼团和优惠券功能让预订模块具备更强的营销能力把推荐模块升级为实时协同过滤使用Redis缓存热门线路和相似度矩阵虚拟体验模块还可以接入全景播放器甚至WebVR设备让线上体验的沉浸感更强。每次扩展都建议做成独立小模块。现有SSMFlask的架构不用推翻主业务继续跑在SSM上新功能如果能用Python快速实现就放进Flask如果涉及核心交易状态就放进Java端。两边通过接口协作开发边界清晰测试也方便。最后说点个人体会。这套系统真正难的地方不在于某个类怎么写而在于把Java和Python两条技术链路缝进同一条业务流程。我第一次做的时候也纠结过要不要全部用Spring写完后来发现两边分开写反而更顺畅。希望这篇内容能帮你少踩一些我踩过的坑。
阅读完成 · 觉得有帮助?