尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SpringBoot旅游线路规划系统:预算约束与地图定位实战解析

SpringBoot旅游线路规划系统:预算约束与地图定位实战解析 简介一份基于SpringBoot的旅游线路规划系统Java毕业设计项目面向计算机相关专业学生与Java开发者覆盖从后台管理到用户地图互动的完整业务场景。压缩包共558个文件、约30MB核心包含Java源代码、SpringBoot配置、MySQL数据库脚本以及前端HTML/CSS/JS页面和XML映射文件还附带演示录屏与项目文档。系统支持管理员下设二级管理员可对旅游景点进行新增、查看和编辑普通用户能通过预算和出行时间获得推荐线路地图模块支持缩放浏览、信息搜索、景点坐标定位、线路与住宿推荐并提供导航导游方向指示。已有157人学习下载项目目录按后端类、前端页面和资源文件清晰划分便于导入集成开发环境直接运行或二次开发可作为毕业设计、课程设计及项目实战的完整参考资料。1. 先从“预算时间”的规划逻辑说起很多人做旅游类毕设时把地图API往页面上一贴再放两个输入框演示时输入“3000元、3天”结果要么返回空列表要么推荐一排超预算的酒店——问题往往不在前端而在后端那一层约束过滤根本没写。这套基于SpringBoot的旅游线路规划系统核心价值恰好落在这里用户输入预算和出行日期后端按约束条件把景点和住宿筛一遍再按热度排序最终生成一条能落地的线路。源码里除了常规的登录拦截、用户管理和景点CRUD还覆盖了地图搜索、坐标定位、导航入口等偏前端地图能力的接口。适合正在做Java毕设的人也能让SpringBootMySQL的入门者看到一个完整系统的模块边界。2. 拆压缩包从Controller命名还原SpringBoot项目结构2.1 类名即模块TravelPonitController 与周边配套拿到压缩包先不急着跑把源码里的Controller类全部列出来项目的模块边界一目了然。这套项目里有TravelPonitController景点模块、NewsController新闻公告、MyTravelController我的行程、ManagerController管理端、MembersController与UsersController会员/用户、LoginController登录以及E3WebMvcConfigurer和LoginInterceptor两个配置与拦截类。Controller模块职责典型路径TravelPonitController景点查询、详情、坐标/point/**MyTravelController我的行程、线路保存/myTravel/**ManagerController景点维护、管理端操作/admin/**MembersController会员注册、信息维护/members/**UsersController用户管理、角色控制/users/**NewsController公告新闻查看/news/**LoginController登录登出、会话创建/login这里要提醒一个细节TravelPonitController里的“Ponit”不是笔误源码里就是这个拼写接手时别统一重命名。一旦改名前端请求路径、数据库字段、录像演示里的操作路径全都要跟着动成本远大于收益。SpringBoot的Web层通常只做参数接收和结果包装Service层处理业务Repository层访问MySQL。多数毕设为了演示方便会把业务堆在Controller里这套项目明显是分层写的按分层方式读就会顺畅很多。2.2 数据库表设计与实体映射数据库是整个系统最先要读的部分。旅游线路规划涉及的不只是景点表还要有用户、酒店、线路主表、线路明细表。CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0, nickname VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE travel_point ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50), ticket_price INT, hot_score INT, latitude DOUBLE, longitude DOUBLE, description TEXT ); CREATE TABLE hotel ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100), price INT, star INT, latitude DOUBLE, longitude DOUBLE ); CREATE TABLE travel_plan ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, budget INT, days INT, start_date DATE, status TINYINT ); CREATE TABLE plan_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, plan_id BIGINT, point_id BIGINT, hotel_id BIGINT, day_index INT );线路主表和明细表拆开设计是为了支持同一用户生成多条历史线路。“我的行程”页面只需要查travel_plan主表点进去再加载plan_detail明细不需要每次重新计算推荐结果。对应到Java实体以景点为例Entity Table(name travel_point) public class TravelPoint { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private String name; private String city; Column(name ticket_price) private Integer ticketPrice; Column(name hot_score) private Integer hotScore; private Double latitude; private Double longitude; }注意Column显式指定列名避免JPA驼峰转下划线规则在极端字段名上失效。这里用的是Spring Data JPA借此说一句选型话题JPA可以通过spring.jpa.hibernate.ddl-autoupdate在启动时自动建表或补列对毕设阶段表结构频繁变动很友好如果换成MyBatis建表脚本就得自己维护当表不存在时自动建表需要额外引入数据库迁移工具或初始化脚本工作量会明显增加。2.3 登录与会话控制LoginInterceptor E3WebMvcConfigurer登录权限是后台接口的守门员。源码里有两个关键类LoginInterceptor负责在请求进入Controller之前做校验E3WebMvcConfigurer负责把拦截器注册进SpringMVC的调用链。Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(/login); return false; } return true; } }这段代码的逻辑是从HttpSession里取loginUser属性取不到就重定向到登录页并拦截本次请求。如果是页面跳转sendRedirect够用如果前端发的是AJAX请求更合适的做法是response.setStatus(401)由前端统一跳转否则用户在页面上点击按钮没反应控制台却已经收到一个302。Configuration public class E3WebMvcConfigurer implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /css/**, /js/**, /img/**, /public/**); } }参数说明addPathPatterns(/**)表示拦截全部路径excludePathPatterns用来放行登录页、静态资源和公开接口。常见的返工点是漏掉静态资源放行配完之后CSS、JS全被拦下页面样式全都丢失。调试时如果发现样式异常第一反应应该看这里。3. 线路规划的核心预算约束、日期过滤与地图定位3.1 把“推荐”做成两步先过滤再排序线路规划在业务上是一个典型的“约束满足排序”问题。用户输入的预算和出行天数是硬约束不符合的直接移出候选集候选集内部再按景点热度排序保证推荐结果“在预算内且值得去”。Service public class RouteRecommendService { private final TravelPointRepository pointRepository; private final HotelRepository hotelRepository; public RouteResult recommend(RouteQuery query) { ListTravelPoint candidates pointRepository.findEligible( query.getBudget(), query.getDays()); if (candidates.isEmpty()) { // 放宽10%再查一次避免“差几十块”导致空列表 candidates pointRepository.findEligible( (int) (query.getBudget() * 1.1), query.getDays()); } candidates.sort(Comparator.comparingInt(TravelPoint::getHotScore).reversed()); return assemble(query, candidates); } private RouteResult assemble(RouteQuery query, ListTravelPoint candidates) { // 按天数分组、选当日住宿、累计费用回写 return null; } }参数说明findEligible的入参是预算和天数对应“景点门票总支出在预算内、每个景点安排一天”的初步筛选hotScore是景点的热度字段降序排列后优先把热门景点放进线路1.1是预算宽放系数处理预算3000、景点门票总计3002这种尴尬场景——差几十块就返回空列表用户会以为系统坏了加一个软阈值比报错友好得多代价只是最终结果可能略微超预算在结果页标注“超出预算约2%”即可。Repository层用Spring Data JPA的Query实现public interface TravelPointRepository extends JpaRepositoryTravelPoint, Long { Query(select t from TravelPoint t where t.ticketPrice :budget) ListTravelPoint findEligible(Param(budget) Integer budget, Param(days) Integer days); }Param(budget)绑定方法入参和JPQL里的命名参数days在这里没有参与SQL而是在Service层做行程分组因为“一天去几个景点”属于业务策略放进SQL里反而把策略写死了。后续想改成“按城市过滤”“按主题过滤”在JPQL里加条件即可不必动调用方。3.2 地图放大缩小、搜索与定位的典型接入方式这套系统的用户端涉及“查看地图、放大缩小、信息搜索、坐标定位”。实际操作里前后端的分工是后端只负责保存和返回景点的经纬度坐标地图渲染与交互全部由前端的地图JS SDK完成。以高德地图为例接入方式比较直接div idtravelMap styleheight: 600px/div script const map new AMap.Map(travelMap, { zoom: 10, center: [116.39, 39.9] }); AMap.plugin([AMap.ToolBar, AMap.Scale, AMap.Geolocation], () { map.addControl(new AMap.ToolBar()); // 提供地图放大/缩小按钮 map.addControl(new AMap.Scale()); map.addControl(new AMap.Geolocation()); // 获取用户当前位置 }); /scriptzoom是地图初始化缩放级别数值越大显示越精细center是初始中心点坐标。ToolBar控件就是页面上看到的放大缩小按钮组Geolocation负责定位到用户当前所在城市解决“用户打开地图不知道自己在哪”的问题。景点标记怎么做后端在页面加载时返回景点坐标列表前端套一层循环生成Marker点击Marker弹出景点详情卡片这是最常见的做法。3.3 住宿推荐为什么不能只按距离排序住宿推荐如果只按“离景点最近”选路线整体预算很容易超支。原因在于住宿推荐是多目标约束距离、价格、评分、行程顺序都要参与。常见做法是抽象一个推荐策略接口让不同策略各自打分最终按总分排序。public interface RecommendStrategy { boolean support(Hotel hotel, RouteContext context); int score(Hotel hotel, RouteContext context); } Component public class ScoreWeightStrategy implements RecommendStrategy { Override public boolean support(Hotel hotel, RouteContext context) { // 酒店价格超过剩余预算就直接淘汰 return hotel.getPrice() context.getRemainBudget(); } Override public int score(Hotel hotel, RouteContext context) { // 星级每高一星加10分价格每100元扣1分 return hotel.getStar() * 10 - hotel.getPrice() / 100; } }support方法做硬性过滤价格超过剩余预算直接淘汰对应前面讲的约束过滤思想score方法做加权打分星级越高加分越多价格越高扣分越多。系数10和100是经验值想调整推荐倾向把这两个系数抽到application.yml里配置即可。这样每个候选酒店都有总分排序取第一个就是当日的“推荐住宿”。4. 管理端权限与景点维护二级管理员怎么落进表里4.1 一级与二级管理员的权限边界摘要里提到的“二级管理员”本质是权限分层的操作角色。普通用户只能查询景点和生成线路一级管理员可以维护景点、酒店和推荐位二级管理员在此基础上还能维护用户状态和新闻公告。权限差在数据库里通常就是一个role字段最简单也最直观。ALTER TABLE sys_user ADD COLUMN role TINYINT DEFAULT 0;role取值约定0表示普通用户1表示一级管理员2表示二级管理员。管理端接口在进入Controller之前检查当前登录用户的role值低于门槛直接返回无权限提示。这里不建议单独建权限表毕设阶段角色数量少建表反而让权限判断变成多表联查复杂度不成比例。4.2 ManagerController 里景点维护的通用写法管理端对景点做“新增、查看、编辑”典型的REST风格接口RestController RequestMapping(/admin) public class ManagerController { private final TravelPointService travelPointService; PostMapping(/point) public ResultVoid addPoint(RequestBody Valid TravelPointDTO dto) { travelPointService.save(dto.toEntity()); return Result.ok(); } GetMapping(/point/{id}) public ResultTravelPointVO getPoint(PathVariable Long id) { return Result.ok(travelPointService.getDetail(id)); } }Valid触发DTO字段校验比如景点名称非空、门票价格大于0RequestBody把前端传来的JSON反序列化成TravelPointDTO对象ResultT是统一响应结构包含code、msg、data三个字段前端根据code是否等于0判断请求是否成功并弹出msg。Controller里收DTOService层映射成Entity避免前端传的值直接落到数据库里。录像演示里新增景点走的就是这个入口实测时用Postman发一条JSON就可以复现。4.3 录像演示和文档的正确打开方式压缩包里带了录像演示最有效的用法是别先读文档开二倍速过一遍主流程记下演示者点了哪些菜单、输入了什么关键词通常十分钟就能确认“线路推荐结果页长什么样”“管理员的入口在哪里”。看完录像再回到文档里核对两件事数据库初始化脚本和默认账号清单。前者决定项目在当前MySQL版本上能不能一次跑通后者决定登录时用什么账号密码。毕业设计排错有相当一部分时间花在“账号不对进不去后台”先看文档能直接省掉这段弯路。5. 打包、部署与三个高频排错点5.1 Maven 打包和启动mvn clean package -DskipTests java -jar target/travel-plan-0.0.1-SNAPSHOT.jar-DskipTests跳过单元测试以缩短打包时间。如果本地装了多个JDK版本先执行mvn -version确认当前默认版本。Spring Boot 2.x基于JDK 8编译Spring Boot 3.x需要JDK 17版本错配时经常报UnsupportedClassVersionError。启动时想动态换端口java -jar target/travel-plan-0.0.1-SNAPSHOT.jar --server.port80815.2 常见问题第一个坑数据库连接串时区没配。Spring Boot连接MySQL 8时URL里缺少serverTimezone会直接报连接失败。参考配置spring: datasource: url: jdbc:mysql://localhost:3306/travel_plan?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jpa: hibernate: ddl-auto: updatecharacterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai告诉驱动当前时区避免MySQL 8默认UTC时区导致的时间差问题。ddl-auto: update让JPA在启动时自动建表或补列省去手动导入SQL的步骤。第二个坑端口被占用。启动时看到Port 8080 was already in use说明有进程占着端口。netstat -ano | findstr :8080拿到PID后到任务管理器结束进程或者在application.yml里直接改server.port。部署到服务器上时记得确认防火墙放行了对应端口。第三个坑Spring Boot版本太高导致依赖报错。Spring Boot 3.x把javax.servlet迁移到了jakarta.servlet旧代码里import javax.servlet.*会编不过报NoClassDefFoundError。接手项目先看pom.xml里的parent版本再决定要不要改import包名。有句话可以记一下Spring Boot版本升大版本很多第三方starter都要同步升不是所有starter都适配了新版。5.3 不依赖前端的接口验证技巧页面还没调通时用curl直接打接口验证后端逻辑curl -X POST http://localhost:8080/api/route/recommend \ -H Content-Type: application/json \ -d {\budget\:3000,\days\:3}返回的JSON里应该包含行程天数、景点列表和每日住宿推荐。如果返回空数组在RouteRecommendService里临时打一行日志打印候选集大小——先确认是过滤条件把数据全筛掉了还是Repository查询本身有问题。把候选集打印出来后手动把预算从3000改成5000再试一次对比两次集合大小的差异就能快速定位是预算约束的问题还是日期判断的问题。本文还有配套的精品资源点击获取
返回列表