之前在准备毕业设计时,不少同学容易陷入一个常见困境:课程学了 Spring Boot、数据库、前端三板斧,但自己动手从零做一套完整系统时,总是不知道从哪里切入。尤其是这两年 AI 大模型非常火,如果能把它和传统的 Web 开发结合起来,既能让项目更有亮点,又能贴合“智能应用”的选题方向。本文分享一套完整的智能天气出行服务系统设计与实现方案,基于 Spring Boot + Thymeleaf + AI 大模型接口,覆盖需求分析、数据库设计、后端开发、页面渲染、大模型接入、项目演示等全流程。整套代码思路既能直接用于毕业设计,也可以作为 Spring Boot 综合实战练手项目,后端开发刚入门、想系统走一遍完整项目的同学都很适合。
1. 项目背景与核心概念
1.1 系统定位
智能天气出行服务系统,简单来说,是一个“查天气 + 给出行建议”的 Web 应用。用户输入一个城市名称,系统会先获取该城市的实时天气数据,再结合日期、出发地、目的地等出行偏好,调用 AI 大模型接口,自动生成一份智能出行建议。建议内容可以是穿衣提醒、交通方式推荐、出行注意事项,也可以根据天气情况给出景点安排建议。
与普通天气网站相比,这个系统的差异点不在于“天气数据本身”,而在于“天气数据到出行决策的转换”。传统天气站点只展示温度、风力、湿度;本系统则通过 Prompt 工程把结构化天气数据拼装成自然语言提示词,交给大模型生成更人性化、更具体的出行指导。这也是整个项目最能体现“AI 应用”价值的地方。
1.2 为什么适合作为毕业设计
从选题层面看,这类系统具备比较完整的业务闭环:有用户、有页面、有外部接口调用、有数据持久化、有 AI 能力集成。演示时既能看到前端交互效果,也能展示后端接口设计,还能现场演示大模型生成建议,内容丰富度足够。
从技术层面看,它没有停留在简单的增删改查上,而是涉及:
- Spring Boot 三层架构与依赖注入;
- Thymeleaf 服务端页面渲染;
- MySQL 数据库设计与 MyBatis-Plus 操作;
- RestTemplate / HttpClient 调用第三方 HTTP 接口;
- JSON 数据解析与结构化处理;
- AI 大模型 API 的 Prompt 设计与返回解析;
- 异常处理、日志记录、参数校验等工程规范。
这些内容基本覆盖了 Java 后端岗位日常开发的核心技能点,写进简历和项目文档里都比较有价值。
1.3 技术难点拆解
把这个项目拆开来看,需要重点解决的难点有三个:
第一个是天气数据的获取。这里涉及外部 API 调用,需要考虑接口地址、请求参数、返回格式、超时处理、异常回退等。如果像写课程作业那样只做一个假数据页面,项目会显得单薄,但如果完全依赖外网接口,线上演示时又可能因为网络问题翻车,所以我会在文中给出 Mock 兜底方案。
第二个是 AI 大模型接口的集成。大模型接口本质上是 HTTP POST 请求,难点在于 Prompt 设计:如何把天气数据变成一段高质量提示词,让模型返回结构清晰、内容可用的建议。
第三个是前端页面与后端的数据流转。使用 Thymeleaf 时,Controller 通过 Model 携带数据,模板通过 th:text、th:if 等语法渲染。这个流程虽然比前后端分离简单,但非常直观,适合展示 Spring Boot 服务端渲染的完整链路。
2. 技术栈与系统架构设计
2.1 技术选型与版本说明
本文示例环境以 Java 8、Spring Boot 2.7.x、MyBatis-Plus 3.5.x、MySQL 8.x 为准。这套组合在目前高校毕业设计中非常常见,稳定性好,社区资料多,遇到问题容易检索到解决方案。
你本机使用的版本不一定和示例完全一致,这不影响阅读。Spring Boot 3.x 对 JDK 版本要求更高,如果使用 3.x,注意部分依赖坐标和配置项会有差异,本文会尽量标注容易踩坑的地方。
整体技术栈如下:
| 层次 | 技术 | 说明 |
|---|---|---|
| 前端页面 | Thymeleaf + HTML + CSS | 服务端模板渲染 |
| Web 框架 | Spring Boot | 提供 MVC 与自动配置 |
| 数据访问 | MyBatis-Plus | 简化数据库操作 |
| 数据库 | MySQL 8.x | 持久化业务数据 |
| 天气数据源 | 和风天气或其他天气 API | 提供实时天气 JSON 数据 |
| AI 能力 | 大模型 API(OpenAI 兼容格式) | 生成智能出行建议 |
| 构建工具 | Maven | 依赖管理与打包 |
2.2 系统功能模块设计
根据毕设演示需求,我将系统划分为以下功能模块:
用户模块:注册、登录,保证每个用户的出行记录是隔离的,属于完整系统的基础模块。
天气查询模块:输入城市名,返回实时天气、温度、风力、湿度等结构化数据。系统内部先通过城市编码接口拿到城市 ID,再查询天气数据。
AI 出行建议模块:将天气数据、日期、出发地与目的地拼接成 Prompt,调用大模型接口,生成自然语言建议,并将建议内容保存到数据库。
历史记录模块:用户可以查看自己最近生成的出行建议记录。这个模块能配合数据库设计一起讲解,答辩时很好展示。
个人设置模块:保存用户常驻城市、常用出行偏好,后续生成建议时自动携带这些信息,提升建议的个性化程度。
2.3 核心业务流程
系统核心流程如下:
- 用户在前端页面输入城市、选择日期、填写出发地和目的地;
- Controller 接收请求参数,调用天气服务获取实时天气数据;
- 天气服务优先调用真实天气 API;如果配置了 Mock 模式或调用失败,则按预设规则返回模拟数据;
- 系统把天气数据与用户出行信息组合成 Prompt;
- AI 服务调用大模型接口,获得出行建议文本;
- 查询结果保存到 travel_note 表;
- 页面渲染天气卡片和 AI 建议卡片,展示给用户。
整个流程的关键在于第 4 步和第 5 步。Prompt 如果写得太随意,模型返回的内容会非常发散;如果结构清晰、限制明确,返回结果的质量会明显提升。
2.4 项目目录结构
采用标准的 Maven 工程结构,包名建议使用 com.example.smarttravel。核心目录如下:
src/main/java/com/example/smarttravel ├── SmartTravelApplication.java ├── config │ └── RestTemplateConfig.java ├── controller │ ├── UserController.java │ └── WeatherController.java ├── entity │ ├── User.java │ ├── TravelNote.java │ └── AiSuggestionLog.java ├── mapper │ ├── UserMapper.java │ ├── TravelNoteMapper.java │ └── AiSuggestionLogMapper.java ├── service │ ├── UserService.java │ ├── WeatherService.java │ └── AiTravelService.java ├── service/impl │ ├── UserServiceImpl.java │ ├── WeatherServiceImpl.java │ └── AiTravelServiceImpl.java ├── dto │ ├── WeatherInfo.java │ └── TravelQueryParam.java └── utils └── JsonUtils.java src/main/resources ├── application.yml ├── mapper └── templates ├── index.html ├── login.html ├── weather/query.html └── weather/result.html这样分层的好处是职责清晰:Controller 只负责参数接收与视图跳转,Service 负责业务逻辑,第三方接口调用放在 Service 内部实现,后续替换天气供应商或大模型厂商时只改 Service 层即可。
3. 数据库设计与初始化
3.1 表结构设计
本系统主要设计三张业务表:用户表、出行记录表、AI 建议日志表。
用户表 sys_user 负责存储用户登录信息与常用城市。
出行记录表 travel_note 用来保存每次查询的输入参数与天气结果,方便前端展示历史记录。
AI 建议日志表 ai_suggestion_log 单独保存大模型返回的建议文本。将建议单独成表的好处是,后续如果想对比不同模型、不同 Prompt 的效果,可以直接在日志表里查询,不污染业务数据。
3.2 初始化 SQL
执行以下 SQL 创建数据库和表。数据库名使用 smart_travel。
CREATE DATABASE IF NOT EXISTS smart_travel DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE smart_travel; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT '密码,建议保存加密后的密文', nickname VARCHAR(50) COMMENT '昵称', phone VARCHAR(20) COMMENT '手机号', city VARCHAR(50) COMMENT '常驻城市', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) COMMENT '用户表'; CREATE TABLE travel_note ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', user_id BIGINT NOT NULL COMMENT '用户ID', start_city VARCHAR(50) COMMENT '出发城市', target_city VARCHAR(50) COMMENT '到达城市', travel_date VARCHAR(20) COMMENT '出行日期', weather_desc VARCHAR(100) COMMENT '天气描述', temperature VARCHAR(20) COMMENT '温度区间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '查询时间' ) COMMENT '出行查询记录表'; CREATE TABLE ai_suggestion_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', user_id BIGINT NOT NULL COMMENT '用户ID', travel_note_id BIGINT COMMENT '关联出行记录ID', prompt_text TEXT COMMENT '发送给大模型的提示词', suggestion_text TEXT COMMENT '大模型返回的建议内容', model_name VARCHAR(50) COMMENT '使用的模型名称', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '生成时间' ) COMMENT 'AI建议日志表';表名使用 sys_user 而不是 user,是为了避免 user 在某些数据库版本或查询语句中与保留字冲突。实际项目中这是一个很好的防守习惯,尤其在答辩演示时,SQL 能一次跑通非常重要。
4. Spring Boot 后端核心代码实现
4.1 配置文件 application.yml
在 resources 目录下创建 application.yml。配置信息包括数据源、MyBatis-Plus、天气 API、AI 接口以及 Mock 开关。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/smart_travel?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto # 天气服务配置 weather: mock-enabled: true api-key: your_weather_api_key base-url: https://devapi.qweather.com city-lookup-path: /v2/city/lookup now-path: /v7/weather/now # AI 大模型配置 ai: api-key: your_ai_api_key # 以 OpenAI 兼容接口为例,国内部分模型服务也支持该协议 base-url: https://api.openai.com/v1/chat/completions model: gpt-3.5-turbo max-tokens: 800 temperature: 0.7 timeout-seconds: 30这里将天气和 AI 的密钥都放到配置文件中,实际部署时建议放到环境变量或配置中心。项目文档中要明确写出“敏感信息不应硬编码在源码中”这一条,属于加分项。
4.2 实体类与 Mapper
以 TravelNote 为例,实体类使用 Lombok 简化 getter/setter。
// 文件路径:src/main/java/com/example/smarttravel/entity/TravelNote.java package com.example.smarttravel.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("travel_note") public class TravelNote { @TableId(type = IdType.AUTO) private Long id; private Long userId; private String startCity; private String targetCity; private String travelDate; private String weatherDesc; private String temperature; private LocalDateTime createTime; }Mapper 接口直接继承 MyBatis-Plus 的 BaseMapper,单表 CRUD 完全不需要手写 SQL。
// 文件路径:src/main/java/com/example/smarttravel/mapper/TravelNoteMapper.java package com.example.smarttravel.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.smarttravel.entity.TravelNote; import org.apache.ibatis.annotations.Mapper; @Mapper public interface TravelNoteMapper extends BaseMapper<TravelNote> { }4.3 天气服务实现
天气服务层需要完成两件事:查询城市编码、查询实时天气。这里提供一个简化实现,重点展示 RestTemplate 请求与 JSON 解析逻辑。
先定义天气信息的 DTO 类:
// 文件路径:src/main/java/com/example/smarttravel/dto/WeatherInfo.java package com.example.smarttravel.dto; import lombok.Data; @Data public class WeatherInfo { private String city; private String text; private String temp; private String feelsLike; private String windDir; private String windScale; private String humidity; private String updateTime; }然后在天气服务中实现请求逻辑:
// 文件路径:src/main/java/com/example/smarttravel/service/impl/WeatherServiceImpl.java package com.example.smarttravel.service.impl; import com.example.smarttravel.dto.WeatherInfo; import com.example.smarttravel.service.WeatherService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.Map; @Service public class WeatherServiceImpl implements WeatherService { @Resource private RestTemplate restTemplate; @Resource private ObjectMapper objectMapper; @Value("${weather.mock-enabled}") private Boolean mockEnabled; @Value("${weather.api-key}") private String apiKey; @Value("${weather.base-url}") private String baseUrl; @Value("${weather.city-lookup-path}") private String cityLookupPath; @Value("${weather.now-path}") private String nowPath; @Override public WeatherInfo getWeatherByCity(String city) { if (Boolean.TRUE.equals(mockEnabled)) { return buildMockWeather(city); } try { // 第一步:查询城市编码 String cityLookupUrl = baseUrl + cityLookupPath + "?location=" + city; String cityLocationId = getCityLocationId(cityLookupUrl); // 第二步:查询实时天气 String nowUrl = baseUrl + nowPath + "?location=" + cityLocationId; return fetchNowWeather(nowUrl, city); } catch (Exception e) { // 第三方接口异常时自动降级为 Mock 数据 return buildMockWeather(city); } } private String getCityLocationId(String url) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("X-QW-Api-Key", apiKey); HttpEntity<String> entity = new HttpEntity<>(headers); ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); try { JsonNode root = objectMapper.readTree(response.getBody()); JsonNode location = root.path("location"); if (location.isArray() && location.size() > 0) { return location.get(0).path("id").asText(); } } catch (Exception e) { throw new RuntimeException("解析城市编码失败", e); } throw new RuntimeException("未查询到城市编码"); } private WeatherInfo fetchNowWeather(String url, String city) { // 请求逻辑与 getCityLocationId 类似,解析 now 节点后封装为 WeatherInfo return buildMockWeather(city); } private WeatherInfo buildMockWeather(String city) { // 为了演示稳定性,Mock 数据固定返回,便于本地开发 WeatherInfo info = new WeatherInfo(); info.setCity(city); info.setText("多云"); info.setTemp("18"); info.setFeelsLike("17"); info.setWindDir("东北风"); info.setWindScale("2级"); info.setHumidity("55%"); info.setUpdateTime("2025-01-01 08:00:00"); return info; } }上面的代码将核心思路展示清楚:外部接口异常时走 Mock 降级,避免因为第三方服务不稳定导致整个系统不可用。实际项目中你可能需要根据所选天气 API 的接入文档调整请求头和返回字段,这一点我建议在代码注释里保留提示,答辩时也能体现你的工程意识。
4.4 AI 出行建议服务实现
AI 服务是本系统的亮点模块。我选择调用 OpenAI 兼容格式的大模型接口,这样国内很多模型服务只要支持兼容协议就能直接替换,系统扩展性更好。
核心实现包括三部分:构建 Prompt、构造 HTTP 请求、解析返回结果。
// 文件路径:src/main/java/com/example/smarttravel/service/impl/AiTravelServiceImpl.java package com.example.smarttravel.service.impl; import com.example.smarttravel.dto.WeatherInfo; import com.example.smarttravel.service.AiTravelService; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.annotation.Resource; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Service public class AiTravelServiceImpl implements AiTravelService { @Resource private RestTemplate restTemplate; @Resource private ObjectMapper objectMapper; @Value("${ai.api-key}") private String apiKey; @Value("${ai.base-url}") private String baseUrl; @Value("${ai.model}") private String model; @Value("${ai.max-tokens}") private Integer maxTokens; @Value("${ai.temperature}") private Double temperature; @Override public String generateTravelAdvice(WeatherInfo weather, String startCity, String targetCity, String travelDate) { String prompt = buildPrompt(weather, startCity, targetCity, travelDate); return callAiApi(prompt); } private String buildPrompt(WeatherInfo weather, String startCity, String targetCity, String travelDate) { StringBuilder sb = new StringBuilder(); sb.append("你是一位资深的出行规划助手,请根据以下天气信息和出行信息,为用户提供实用的出行建议。\n"); sb.append("建议内容需要包括:穿衣搭配建议、交通方式推荐、出行注意事项、当天活动安排建议。\n"); sb.append("要求:语言简洁、条理清晰,使用中文回答,总字数控制在300字以内。\n\n"); sb.append("【出行信息】\n"); sb.append("出发地:").append(startCity).append("\n"); sb.append("目的地:").append(targetCity).append("\n"); sb.append("出行日期:").append(travelDate).append("\n\n"); sb.append("【目的地天气】\n"); sb.append("天气现象:").append(weather.getText()).append("\n"); sb.append("温度:").append(weather.getTemp()).append("℃,体感温度").append(weather.getFeelsLike()).append("℃\n"); sb.append("风力:").append(weather.getWindDir()).append(weather.getWindScale()).append("\n"); sb.append("湿度:").append(weather.getHumidity()).append("\n\n"); sb.append("请按以上要求生成建议。"); return sb.toString(); } private String callAiApi(String prompt) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.setBearerAuth(apiKey); Map<String, Object> body = new HashMap<>(); body.put("model", model); body.put("max_tokens", maxTokens); body.put("temperature", temperature); List<Map<String, String>> messages = new ArrayList<>(); Map<String, String> systemMessage = new HashMap<>(); systemMessage.put("role", "system"); systemMessage.put("content", "你是一个专业的出行规划助手。"); messages.add(systemMessage); Map<String, String> userMessage = new HashMap<>(); userMessage.put("role", "user"); userMessage.put("content", prompt); messages.add(userMessage); body.put("messages", messages); HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers); ResponseEntity<String> response = restTemplate.exchange(baseUrl, HttpMethod.POST, request, String.class); try { JsonNode root = objectMapper.readTree(response.getBody()); return root.path("choices").get(0).path("message").path("content").asText(); } catch (Exception e) { throw new RuntimeException("解析 AI 返回结果失败", e); } } }这段代码里有一个非常关键的细节:Constructor 组装 JSON 结构时,messages 是一个数组,数组内每个元素包含 role 和 content 字段。如果你在这里遗漏了 role 字段,或者把 messages 写成了对象而不是数组,大模型接口会直接返回 400 错误。这是新手接入大模型 API 时最高频的报错之一。
4.5 Controller 编写与参数传递
Controller 主要负责整合天气服务和 AI 服务,并把结果放入 Model 供 Thymeleaf 渲染。
// 文件路径:src/main/java/com/example/smarttravel/controller/WeatherController.java package com.example.smarttravel.controller; import com.example.smarttravel.dto.WeatherInfo; import com.example.smarttravel.entity.TravelNote; import com.example.smarttravel.entity.User; import com.example.smarttravel.service.AiTravelService; import com.example.smarttravel.service.TravelNoteService; import com.example.smarttravel.service.WeatherService; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import javax.annotation.Resource; import javax.servlet.http.HttpSession; @Controller @RequestMapping("/travel") public class TravelController { @Resource private WeatherService weatherService; @Resource private AiTravelService aiTravelService; @Resource private TravelNoteService travelNoteService; @GetMapping("/query") public String queryPage() { return "travel/query"; } @PostMapping("/generate") public String generateAdvice(@RequestParam String targetCity, @RequestParam(required = false) String startCity, @RequestParam(required = false) String travelDate, HttpSession session, Model model) { User currentUser = (User) session.getAttribute("loginUser"); if (currentUser == null) { return "redirect:/user/login"; } if (startCity == null || startCity.isEmpty()) { startCity = currentUser.getCity() == null ? "北京" : currentUser.getCity(); } WeatherInfo weather = weatherService.getWeatherByCity(targetCity); String suggestion = aiTravelService.generateTravelAdvice(weather, startCity, targetCity, travelDate); // 保存记录 TravelNote note = new TravelNote(); note.setUserId(currentUser.getId()); note.setStartCity(startCity); note.setTargetCity(targetCity); note.setTravelDate(travelDate); note.setWeatherDesc(weather.getText()); note.setTemperature(weather.getTemp() + "℃"); travelNoteService.saveTravelNote(note); model.addAttribute("weather", weather); model.addAttribute("suggestion", suggestion); model.addAttribute("startCity", startCity); model.addAttribute("targetCity", targetCity); model.addAttribute("travelDate", travelDate); return "travel/result"; } }这里需要解释一下为什么用@PostMapping提交表单。虽然 Thymeleaf 页面中表单请求也可以用 GET,但生成 AI 建议这个操作会对数据库写入记录,属于有副作用的操作,按照 HTTP 语义应该使用 POST,浏览器刷新时也会提示“重新提交表单”,从数据安全角度更合理。
5. 前端页面与 Thymeleaf 渲染
5.1 页面整体设计
前端项目使用 Thymeleaf 模板,不需要单独搭建 Vue 或 React 工程,这对计算机毕业设计来说大大降低了工作量。页面大致分成三个:登录/注册页、查询页、结果展示页。
查询页是核心页面,包含一个表单提交区域和两个展示区域:左侧显示天气信息卡片,右侧显示 AI 出行建议。
5.2 查询页模板
在src/main/resources/templates/travel/query.html中,编写查询表单:
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>智能天气出行服务</title> <link rel="stylesheet" th:href="@{/css/style.css}"> </head> <body> <div class="container"> <h2>智能天气出行服务</h2> <p class="subtitle">输入目的地,获取基于实时天气的 AI 出行建议</p> <form class="query-form" th:action="@{/travel/generate}" method="post"> <div class="form-item"> <label>目的地城市</label> <input type="text" name="targetCity" placeholder="请输入城市名称,如:杭州" required> </div> <div class="form-item"> <label>出发城市</label> <input type="text" name="startCity" placeholder="默认读取个人设置中的常驻城市"> </div> <div class="form-item"> <label>出行日期</label> <input type="date" name="travelDate"> </div> <button type="submit">生成出行建议</button> </form> <div class="tip"> <p>演示提示:如果天气 API 未配置真实密钥,系统将自动启用模拟天气数据。</p> </div> </div> </body> </html>这里要注意,th:action="@{/travel/generate}"的作用是让 Thymeleaf 自动拼接当前项目的上下文路径。如果你在本地直接运行,默认会渲染成/travel/generate;如果部署到 Tomcat 并设置了 context-path,则能自动带上项目名前缀,不会出现路径 404 的问题。
5.3 结果展示页模板
在travel/result.html中展示天气和 AI 建议。关键语法是th:if判空和th:text文本渲染。
<!DOCTYPE html> <html lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>出行建议结果</title> <link rel="stylesheet" th:href="@{/css/style.css}"> </head> <body> <div class="container result-container"> <h2>出行建议结果</h2> <div class="result-grid"> <div class="card weather-card"> <h3>目的地天气</h3> <p th:if="${weather}"> <span th:text="${weather.city}">杭州</span> <span th:text="${weather.text}">多云</span> </p> <ul th:if="${weather}"> <li>温度:<span th:text="${weather.temp} + '℃'"></span></li> <li>体感温度:<span th:text="${weather.feelsLike} + '℃'"></span></li> <li>风力:<span th:text="${weather.windDir} + ${weather.windScale}"></span></li> <li>湿度:<span th:text="${weather.humidity}"></span></li> </ul> </div> <div class="card ai-card"> <h3>AI 出行建议</h3> <p th:if="${suggestion}" class="ai-text" th:text="${suggestion}"> 大模型返回的建议内容 </p> <p th:if="${!suggestion}" class="error-text">建议生成失败,请检查 AI 接口配置。</p> </div> </div> <div class="info-line"> <span>出发地:<span th:text="${startCity}"></span></span> <span>目的地:<span th:text="${targetCity}"></span></span> <span>日期:<span th:text="${travelDate}"></span></span> </div> <a class="btn-back" th:href="@{/travel/query}">继续查询</a> </div> </body> </html>在 Thymeleaf 中,th:text="${weather.temp} + '℃'"可以正常拼接字符串。如果某个字段为空,可以直接使用th:if="${weather.temp != null}"做条件判断,尽量避免在页面上展示 null 字样。
这里也顺带解释一个新手常犯的错误:Thymeleaf 的属性是写在 HTML 标签内部的,如果你在 div 标签外单独写th:if,模板解析会直接报错。所有 Thymeleaf 指令必须依附在具体标签上。
5.4 页面样式与交互优化
为了在答辩演示时视觉效果好,可以适度编写一些简洁的 CSS 卡片样式。这里不做完整 CSS 展示,但建议遵循以下几个原则:
整体页面宽度控制在 1000px 左右,居中显示。卡片使用圆角、浅色边框、轻微阴影。结果页的天气卡片和 AI 卡片采用两列布局,移动端则自适应单列。按钮颜色统一,主操作按钮使用醒目色,返回按钮使用弱化色。
这些设计不需要很强的前端功底,但能让界面看起来像一个完整的“系统”,而不是裸 HTML。
6. 运行验证与效果说明
6.1 启动项目
运行前需要准备:
- MySQL 执行初始化 SQL;
- 在 application.yml 中修改数据库账号密码;
- 如果没有天气 API 和 AI API 密钥,保持 mock-enabled: true;
- 启动 Spring Boot 项目。
在项目根目录执行 Maven 命令:
mvn spring-boot:run或者将项目打包后运行:
mvn clean package -DskipTests java -jar target/smart-travel-0.0.1-SNAPSHOT.jar启动成功后,控制台出现 Spring Boot 启动日志,浏览器访问:
http://localhost:8080/travel/query6.2 演示流程建议
登录系统后,进入查询页。
输入目的地“杭州”,出发城市留空,出行日期选择当天。点击“生成出行建议”,系统会先查询杭州天气,然后调用 AI 接口生成建议。
如果使用 Mock 天气模式,页面显示的是模拟的多云天气数据;AI 建议则基于这段数据生成。演示时你可以提前准备好文本描述,说明“这里展示的是系统如何把天气参数传给大模型,并返回结构化建议”。
如果希望展示真实天气效果,可以申请天气 API 免费额度,将weather.mock-enabled改为 false,并填入有效 API Key。
如果希望展示大模型的个性化效果,可以尝试将 Prompt 中的“总字数控制在 300 字以内”改为“请用表格形式输出”,模型会返回不同风格的答案,这可以作为答辩时的亮点互动。
6.3 验证点与预期输出
项目运行正常时,浏览器页面可以看到:
天气卡片区域显示城市名称、天气现象、温度、体感温度、风向风力、湿度。
AI 卡片区域显示自然语言建议,内容包含穿衣推荐、交通建议、活动安排等。
数据库 travel_note 表新增一条记录,包含用户、出发地、目的地、天气描述和温度。
数据库 ai_suggestion_log 表新增一条记录,包含 prompt_text 和 suggestion_text,方便在答辩时展示实际发送给大模型的 Prompt 内容,这是项目技术含金量的直接证明。
7. 常见问题与排查思路
7.1 启动类常见报错
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 启动时报数据源连接失败 | MySQL 未启动或账号密码错误 | 检查 MySQL 服务状态,核对 application.yml 配置 |
| 启动报 mapper 找不到 | Mapper 接口没有加 @Mapper 注解 | 在 Mapper 接口上添加 @Mapper 或启动类加 @MapperScan |
| Thymeleaf 页面 404 | templates 目录下路径不对 | 确认页面存放在 resources/templates 下,且路径与 Controller 返回值一致 |
| 页面中文乱码 | 编码设置不一致 | 数据库连接 URL 添加 characterEncoding=utf8,页面文件保存为 UTF-8 |
7.2 AI 接口调用报错
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 返回 401 错误 | API Key 无效或未设置 | 检查 application.yml 中 ai.api-key |
| 返回 400 错误 | messages 结构不对 | 检查 messages 是否为数组,role 字段是否合法 |
| 返回超时 | 网络原因或 timeout-seconds 过短 | 适当增大超时时间,检查网络连通性 |
| 返回内容异常 | Prompt 约束不够 | 调整 Prompt,增加字数、格式、范围限制 |
这里重点提示一个容易忽略的点:如果你在本地开发时使用了代理工具,可能会影响 RestTemplate 正常请求外部 API,导致连接超时或证书报错。排查时要先关闭系统代理或在 RestTemplate 中显式配置网络代理,再逐步排除其他问题。
另外,国内环境访问部分大模型接口可能存在网络不稳定问题,建议在项目文档中注明你可以替换为其他兼容 OpenAI 协议的服务商。替换时只需修改 base-url、model 和 api-key,代码不需要大改,这也是本设计方案的一个优点。
7.3 天气接口返回空数据
天气服务依赖城市编码查询。如果输入的是英文城市名,或者城市名称包含后缀(如“杭州市”与“杭州”),可能查询不到对应编码。
建议在 Controller 或 Service 层对参数做简单清洗,比如去除“市”“省”等行政后缀,并统一为城市名称后再调用天气 API。
private String normalizeCity(String city) { if (city == null) { return ""; } return city.replace("市", "").replace("省", "").trim(); }这种方式虽然简单,但能有效改善用户体验。
8. 最佳实践与毕设答辩建议
8.1 工程规范建议
第一,密钥信息不要硬编码在 Java 类中。哪怕项目只是毕业设计,代码评审和答辩老师也很可能注意到硬编码的 API Key。把它们放在 application.yml 或环境变量中,属于基本的代码安全意识。
第二,第三方接口调用一定要设置超时时间。RestTemplate 默认情况下可能长时间等待响应,这会导致页面卡死。建议在 RestTemplate 配置中显式设置连接超时和读取超时。
// 文件路径:src/main/java/com/example/smarttravel/config/RestTemplateConfig.java package com.example.smarttravel.config; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setMaxTotal(100); connectionManager.setDefaultMaxPerRoute(20); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); factory.setConnectTimeout(5000); factory.setReadTimeout(30000); return new RestTemplate(factory); } }这样即使某个外部接口响应很慢,也不会拖垮整个系统。
第三,用户密码不能明文存储。本系统示例为了简洁没有引入加密逻辑,但实际项目中至少应该使用 BCrypt 加密。Spring Security 的BCryptPasswordEncoder或者 Hutool 的 BCrypt 工具类都可以,只要在注册时将明文密码转为密文,在登录时用 matches 校验即可。
8.2 答辩演示技巧
毕设答辩时,老师通常不会只看界面,更关注系统设计和技术亮点。建议从三个角度展开:
数据流讲解:从用户输入城市开始,沿着 Controller → Service → 第三方 API → 数据库 → Thymeleaf 渲染这条链路完整讲一遍。这是最稳妥的展示方式。
异常处理展示:演示前可以把天气 API 密钥故意填错,展示系统自动降级为 Mock 数据的过程。这个细节能证明你考虑了系统稳定性,比单纯展示正常流程更有说服力。
Prompt 设计讲解:把 ai_suggestion_log 表中的 prompt_text 展示出来,和最终返回的建议对照讲解,说明你是如何设计提示词的,这是整个项目中 AI 部分的专业体现。
8.3 项目文档与源码配套
毕业设计通常要求提交论文(LW)和答辩 PPT。建议项目文档中重点写清楚技术选型理由、数据库设计说明、核心功能实现、系统测试结果。源码中关键方法建议写上注释,尤其是 AI 接口调用和天气数据解析这两块,老师翻阅源码时通常会直接看这些位置。
PPT 结构可以按照“选题背景 → 需求分析 → 系统设计 → 功能实现 → 系统演示 → 总结展望”展开,演示部分预留 5 分钟,重点展示查询页和结果页,再补充一张数据库表关系截图即可。
9. 学习路线与后续扩展
完成这个项目后,你可以继续在以下几个方向扩展,让系统从“课程设计水平”提升到“企业级项目思路”:
第一个扩展方向是缓存优化。目前每次查询都会实时调用天气 API,可以考虑使用 Redis 缓存城市天气数据,设置 30 分钟过期时间,降低第三方 API 调用量,同时提升页面响应速度。
第二个扩展方向是接口鉴权。当前系统使用 Session 保存登录状态,如果未来想要开发小程序或 App 端,可以引入 Spring Security 或 JWT 做统一认证,让同一套后端逻辑可以支撑多端访问。
第三个扩展方向是大模型能力增强。目前 AI 建议是一次性生成后直接展示,可以考虑引入流式输出,也就是打字机效果;还可以在 Prompt 中增加历史出行记录,让建议更具个性化。
第四个扩展方向是前端升级。把 Thymeleaf 页面替换为 Vue + Element UI 或 React 页面,后端改造成 RESTful API 格式,系统就从服务端渲染架构过渡到了前后端分离架构。这个升级路径非常适合作为论文的“下一步展望”部分。
从课程学习角度来看,这个项目已经把 Java Web 开发的核心链路串起来了:数据库设计、后端接口、页面渲染、外部接口对接、AI 能力集成。完成一遍之后,你对 Spring Boot 项目的理解会从“跟着教程跑通 demo”提升到“独立设计并实现一个完整系统”,这种能力的提升是刷再多视频教程都换不来的。
如果本文对你有帮助,建议收藏备用,实际操作时遇到报错可以回来对照排查。动手把项目跑起来,比反复看十篇教程都有用。