SpringBoot天气管理系统:JPA查询、RESTful接口与数据可视化
2026/9/16 17:41:31 网站建设 项目流程

简介:这是一套基于SpringBoot的天气信息管理系统毕业设计资源,内含项目完整源码与毕业论文,面向Java学习者、毕业设计及课程设计参考者。系统实现天气信息的前台展示、按日期搜索、后台增删改查以及管理员权限管理,覆盖SpringBoot RESTful API、JPA/MyBatis数据库操作、第三方天气接口对接、ECharts图表展示等开发关键点,适合掌握Java基础、希望系统实践Spring Boot开发的读者。RAR压缩包共343个文件,大小约8.97MB,以Java源码、class文件、XML配置、HTML/CSS/JavaScript前端页面、SQL脚本、图片素材等为主,目录结构清晰,便于按模块阅读。已有158人学习。通过这套资源可完整了解一个Spring Boot项目从数据库表设计、后端接口到前端交互的实现过程,代码分层与业务模块划分明确,配套论文也能辅助快速梳理设计思路,完成项目说明与答辩准备。

1. 从查询需求到SpringBoot落地:天气管理系统的技术切面

天气信息管理系统看起来只是把温度湿度存进数据库再查出来,真正动手拆的时候你会发现,它把Java Web开发的所有基础能力都串起来了:日期查询的前后端交互、RESTful接口设计、ORM映射、第三方开放数据接入、定时任务、登录权限控制,一个不少。这也是很多毕业设计和入门项目选它的原因。SpringBoot在这里不是锦上添花,它的自动配置和starter机制把上面这些组件的连接成本降到了最低,让你把精力放在业务边界而不是框架集成上。如果你正在学习Java后端,或者需要一个能讲清楚每个模块的实战源码,这篇是值得读完的。

2. 天气表结构与JPA实体映射的关键配置

2.1 天气信息表的结构与字段设计

项目里最核心的业务对象是天气记录,对应的实体在源码中是TianqiInfo。设计这张表时有一个容易踩的坑:只把"日期"和"城市"当普通字段存,忘记加唯一约束,结果同一个城市同一天的天气被录入了多条,前台按日期一搜,列表里全是重复数据。

我一般会在建表时就加上uk_city_date联合唯一索引,从数据库层面兜底。下面这份建表SQL可以直接用在MySQL 8.x:

CREATE TABLE `tianqi_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `city_code` varchar(32) NOT NULL COMMENT '城市编码,如101010100', `city_name` varchar(64) NOT NULL COMMENT '城市名称', `weather_date` date NOT NULL COMMENT '天气日期', `temperature` decimal(5,2) DEFAULT NULL COMMENT '实时气温(℃)', `low_temp` decimal(5,2) DEFAULT NULL COMMENT '最低气温', `high_temp` decimal(5,2) DEFAULT NULL COMMENT '最高气温', `humidity` decimal(5,2) DEFAULT NULL COMMENT '相对湿度(%)', `wind_direction` varchar(16) DEFAULT NULL COMMENT '风向', `wind_level` varchar(16) DEFAULT NULL COMMENT '风力等级', `create_by` varchar(64) DEFAULT NULL COMMENT '创建人', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_city_date` (`city_code`, `weather_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='天气信息表';

字段类型有几个值得注意的地方:温度用decimal(5,2),足以覆盖从 -99.99 到 999.99 的范围,避免使用float带来的精度漂移;weather_datedate而不是datetime,因为按日搜索时datetime会额外引入时间部分的匹配问题;city_code单独存编码而city_name冗余一份,是为了后续对接第三方天气API时能直接用城市编码做参数,不用每次查表转换。

2.2 SpringBoot与JPA的依赖配置

项目选择Spring Data JPA而不是MyBatis,原因是这个业务场景几乎全是单表操作:插入一条天气记录、按日期查询列表、按主键修改或删除。JPA的派生查询方法在这种场景下比手写XML映射文件效率高很多。另外JPA的ddl-auto配置能根据实体类自动生成表结构,对前期原型阶段特别友好。

pom.xml里两个必要的依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

注意第二行,新版SpringBoot 3.x中MySQL驱动坐标已经从mysql-connector-java改为mysql-connector-j,旧坐标会导致依赖找不到或驱动类报错。application.yml配置如下:

spring: datasource: url: jdbc:mysql://localhost:3306/weather_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: validate show-sql: true open-in-view: false

ddl-auto有五个可选值,生产环境用validate只校验表结构不修改,开发阶段用update可以在表不存在时自动建表,createcreate-drop每次启动都重建表,只适合测试环境。open-in-view: false是一个很多人忽略的配置,它避免视图渲染期间数据库连接一直不释放的问题。show-sql: true我建议只在本地调试时开,线上开会导致日志量成倍增长。

2.3 实体映射与Repository查询方法

实体类对应数据库表,字段映射用JPA标准注解即可:

@Entity @Table(name = "tianqi_info") public class TianqiInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "city_code", nullable = false, length = 32) private String cityCode; @Column(name = "city_name", nullable = false, length = 64) private String cityName; @Column(name = "weather_date", nullable = false) private LocalDate weatherDate; @Column(name = "temperature") private BigDecimal temperature; @Column(name = "low_temp") private BigDecimal lowTemp; @Column(name = "high_temp") private BigDecimal highTemp; @Column(name = "humidity") private BigDecimal humidity; @Column(name = "wind_direction", length = 16) private String windDirection; @Column(name = "wind_level", length = 16) private String windLevel; // 省略 getter / setter }

LocalDate类型对应数据库的date字段,BigDecimal对应decimal(5,2),这种映射关系比直接用DateDouble更严谨,也避免精度转换的隐藏问题。Repository接口的写法是这个项目的精髓:

public interface TianqiInfoRepository extends JpaRepository<TianqiInfo, Long> { List<TianqiInfo> findByWeatherDate(LocalDate weatherDate); Optional<TianqiInfo> findByCityCodeAndWeatherDate(String cityCode, LocalDate weatherDate); @Query("select t from TianqiInfo t where t.weatherDate between :start and :end order by t.weatherDate asc") List<TianqiInfo> findRange(@Param("start") LocalDate start, @Param("end") LocalDate end); }

findByWeatherDate完全靠方法名推导查询逻辑,findByCityCodeAndWeatherDate是复合条件,返回Optional是为了避免调用方收到null然后空指针。findRange用于图表模块,它接收一个日期区间并返回有序结果,@Query里的JPQL对应的是实体属性名而不是数据库列名,写错大小写会直接启动失败。

3. 按日期搜索接口、RESTful查询与ECharts可视化

3.1 按日期搜索的后端接口实现

源码中的TianqiInfoController承担了前台查询入口。设计上我遵循一个原则:前端传原始字符串,后端负责解析和校验,不把Date类型直接暴露给前端。接口实现如下:

@RestController @RequestMapping("/api/tianqi") public class TianqiInfoController { private final TianqiInfoRepository repository; public TianqiInfoController(TianqiInfoRepository repository) { this.repository = repository; } @GetMapping("/search") public Result<List<TianqiInfo>> searchByDate(@RequestParam("date") String date) { LocalDate queryDate = LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE); List<TianqiInfo> list = repository.findByWeatherDate(queryDate); return Result.success(list); } }

逻辑说明:LocalDate.parse使用ISO标准格式yyyy-MM-dd解析字符串,前端传2025-06-01没问题,但如果传成2025/06/01会抛出DateTimeParseException。格式校验放后端的好处是任何客户端都得遵守同一规则,不能只靠前端input[type=date]的约束。接口参数约定如下表:

参数名类型必填说明示例
dateStringISO格式日期,范围 1900-01-01 以后2025-06-01

返回结构统一为Result包装类,包含codemessagedata三个字段。成功时code=200,业务异常时返回code=500和具体提示信息。统一包装的好处是前端处理逻辑简单:只需要判断code是否为200,不需要为每个接口单独处理异常形态。

3.2 前端页面与JavaScript交互

前台页面分两块区域:搜索区放在页面上方,天气结果列表和温度趋势图放在下方。搜索区用原生HTML搭配JavaScript的fetch完成数据请求,不引入Vue或React这类框架。这是合理的,因为页面状态很简单,不需要双向绑定。

<div class="search-bar"> <input type="date" id="weatherDate" value="2025-06-01"> <button id="searchBtn">查询天气</button> </div> <div id="weatherList"></div>
document.getElementById('searchBtn').addEventListener('click', function () { const date = document.getElementById('weatherDate').value; if (!date) { alert('请先选择日期'); return; } fetch('/api/tianqi/search?date=' + encodeURIComponent(date)) .then(response => response.json()) .then(result => { if (result.code === 200) { renderWeatherList(result.data); } else { console.error('查询失败:', result.message); } }) .catch(error => console.error('网络异常:', error)); }); function renderWeatherList(list) { const container = document.getElementById('weatherList'); container.innerHTML = list.map(item => ` <div class="weather-card"> <h3>${item.cityName}(${item.weatherDate})</h3> <p>气温:${item.lowTemp} ~ ${item.highTemp} ℃</p> <p>湿度:${item.humidity} %</p> <p>风向:${item.windDirection} ${item.windLevel}</p> </div> `).join('') || '<p>当天暂无天气记录</p>'; }

encodeURIComponent对日期字符串做编码,虽然日期里没有特殊字符,但这是一个好习惯,防止未来参数出现冒号或空格时请求被截断。renderWeatherList用模板字符串拼HTML,数据为空时显示占位提示。这里有个容易被忽视的细节:fetch默认不带cookie,如果后端接口后续加了登录拦截,需要在请求中加credentials: 'same-origin'

3.3 ECharts展示温度趋势

源码中的EchartsController和前端图表组件配合,解决的是"天气变化趋势"问题。按日期搜索只能看到单点数据,业务上还希望展示最近一周的温度曲线。后端接口设计为返回近N天的最高温和最低温数组:

@GetMapping("/trend") public Result<Map<String, Object>> trend(@RequestParam("days") int days) { LocalDate today = LocalDate.now(); LocalDate start = today.minusDays(days - 1); List<TianqiInfo> list = repository.findRange(start, today); List<String> dates = new ArrayList<>(); List<BigDecimal> highTemps = new ArrayList<>(); List<BigDecimal> lowTemps = new ArrayList<>(); for (TianqiInfo info : list) { dates.add(info.getWeatherDate().toString()); highTemps.add(info.getHighTemp()); lowTemps.add(info.getLowTemp()); } Map<String, Object> data = new HashMap<>(); data.put("dates", dates); data.put("highTemps", highTemps); data.put("lowTemps", lowTemps); return Result.success(data); }

前端ECharts配置最关键的点是xAxisdata必须与seriesdata下标一一对应,否则折线图会出现错位或者断点:

const chart = echarts.init(document.getElementById('trendChart')); const response = await fetch('/api/tianqi/trend?days=7').then(res => res.json()); const data = response.data; chart.setOption({ tooltip: { trigger: 'axis' }, legend: { data: ['最高气温', '最低气温'] }, xAxis: { type: 'category', data: data.dates }, yAxis: { type: 'value', name: '温度(℃)' }, series: [ { name: '最高气温', type: 'line', data: data.highTemps, smooth: true }, { name: '最低气温', type: 'line', data: data.lowTemps, smooth: true } ] });

图表模式下,日期字段在坐标上最好按照时间顺序排列,所以后端@Query里必须加order by t.weatherDate asc,不排序的结果会导致折线交叉错乱。如果某一天没有数据,接口返回的数组会出现空值,ECharts默认会断开连线,可以在series上配置connectNulls: true让趋势线跨过缺失点。

4. 第三方天气数据接入:OkHttp调用、JSON解析与定时同步

4.1 数据源选型:为什么接第三方接口

项目前端展示和历史查询需要数据支撑,如果全部靠管理员手工录入,数据量根本填不满图表和搜索页,而且手工录入的气象数据可信度存疑。常见的做法是后台只保留"管理员修正"能力,数据初始化来自第三方天气开放平台。这样可以同时验证两种能力:调用外部API、解析结构化数据,以及后台的增删改查权限控制。

选型时要注意两点:一是免费接口通常有每分钟调用次数限制,二是不同平台的温度返回字段名不一样,有的叫temp,有的叫temperature。代码中我一般把外部接口的响应先解析成通用对象,再映射到TianqiInfo实体,避免第三方字段变化污染核心业务模型。

4.2 OkHttp调用与Jackson解析

用OkHttp作为HTTP客户端,因为它的连接池和超时控制比原生HttpURLConnection更直观。请求第三方接口并解析JSON的完整过程如下:

@Service public class WeatherSyncService { private final OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); private final ObjectMapper objectMapper = new ObjectMapper(); public TianqiInfo fetchByCity(String cityCode, String apiKey) throws IOException { String url = "https://api.example.com/v3/weather" + "?location=" + cityCode + "&key=" + apiKey; Request request = new Request.Builder() .url(url) .header("Accept", "application/json") .get() .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("API request failed: " + response.code()); } String responseBody = response.body().string(); JsonNode root = objectMapper.readTree(responseBody); JsonNode now = root.path("now"); JsonNode yesterday = root.path("yesterday"); return convert(now, yesterday); } } private TianqiInfo convert(JsonNode now, JsonNode yesterday) { TianqiInfo info = new TianqiInfo(); info.setTemperature(now.path("temp").decimalValue()); info.setHumidity(now.path("humidity").decimalValue()); info.setWindDirection(now.path("windDir").asText()); info.setWindLevel(now.path("windScale").asText()); return info; } }

connectTimeoutreadTimeout必须分开设置,连接超时指建立TCP连接的过程,读超时指服务端返回数据的时间。很多对接问题出在只设置了连接超时,结果接口响应卡住时线程白白占着不释放。objectMapper.readTree方式不预先定义DTO,直接把JSON解析成树形结构,适合字段经常变动的外部接口;如果接口结构稳定且需要字段校验,使用readValue配合DTO更合适。

try (Response response = ...)写法会自动释放连接,这个不能省略,否则高压调用时连接池会被占满。response.body().string()只能调用一次,需要复用响应体时要先把字符串存下来。另外外部接口返回的temp字段可能是浮点数字符串,用decimalValue()解析时要注意字段为null的情况,path("temp")get("temp")安全,后者在字段不存在时返回null导致空指针。

4.3 定时同步与失败隔离

数据同步不能靠管理员手动点按钮,项目里用Spring的@Scheduled注解实现每天定时拉取。最简单的写法如下:

@Scheduled(cron = "0 0 4 * * ?") public void syncDailyWeather() { List<String> cityCodes = cityRepository.findAllCityCodes(); for (String cityCode : cityCodes) { try { TianqiInfo info = fetchByCity(cityCode, apiKey); info.setWeatherDate(LocalDate.now()); repository.findByCityCodeAndWeatherDate(cityCode, LocalDate.now()) .ifPresentOrElse( existing -> update(existing, info), () -> repository.save(info) ); } catch (Exception e) { log.error("同步城市 {} 天气数据失败: {}", cityCode, e.getMessage()); } } }

cron表达式说明:

cron表达式执行时机适用场景
0 0 4 * * ?每天凌晨4点常规天气数据同步
0 0/30 * * * ?每30分钟一次实时温度刷新
0 0 2,14 * * ?每天2点和14点早晚两次数据修正

选凌晨4点同步的考虑是:第三方天气平台凌晨会更新当天的预报数据,但4点的访问量低,不容易触发限流。forEach循环内每个城市单独try-catch,这是失败隔离的关键:一个城市同步失败不会阻断其他城市。如果使用parallelStream并行同步,需要确认第三方API的QPS限制,免费接口一般不建议超过每秒1次。

幂等性处理看findByCityCodeAndWeatherDate的返回值:已存在则更新,不存在则插入。这里依赖前面说的uk_city_date唯一索引兜底,即使并发调用也不会产生重复记录。如果项目部署在多实例环境,@Scheduled会在每个实例都触发一次,需要引入分布式锁(如Redis的setnx)或者用ShedLock控制,单实例部署可以跳过。

5. 权限拦截、接口防刷与联调排错技巧

5.1 角色权限设计与Token拦截

后台管理模块涉及AdminInfoController(管理员管理)、UserInfoController(用户管理)、AccountController(账号登录)。设计思路上,管理员账号和前台用户账号分开两张表,避免互相干扰。接口层面的权限控制常见做法是自定义拦截器,放行登录和查询接口,拦截管理相关的写操作:

@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().startsWith("/api/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !TokenService.validate(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); return false; } return true; } }

拦截器注册的时候要同时配置URL规则:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/login", "/api/tianqi/search"); } }

excludePathPatterns放行前台天气查询接口,因为用户不登录也能看天气;管理端接口(如新增、删除天气记录)由拦截器统一校验token。注意拦截器的preHandle返回false后请求直接终止,不会进入控制器方法,因此也不需要额外的业务逻辑防重入。

5.2 高频问题排查与验证技巧

联调时最常遇到的几个问题,乱码、日期解析、JPA方法名对应不上,表现各异但定位思路是相同的,看日志、看请求参数、看SQL执行情况。用curl可以直接验证接口行为:

curl -i "http://localhost:8080/api/tianqi/search?date=2025-06-01" curl -i -X POST "http://localhost:8080/api/admin/tianqi" \ -H "Authorization: Bearer your_token" \ -H "Content-Type: application/json" \ -d '{"cityCode":"101010100","cityName":"北京","weatherDate":"2025-06-02","temperature":28.5}'

第一条curl验证查询接口的响应状态码和JSON结构,第二条验证需要权限的写入接口。如果返回401,先检查token是否过期;返回400,看是参数缺了还是格式不对;返回500,去后端日志查异常堆栈。-i参数会显示响应头,能快速判断是网络层问题还是业务层问题。

查询接口性能如果出现明显的响应变慢,优先检查是不是缺失索引导致的全表扫描,EXPLAIN SELECT * FROM tianqi_info WHERE weather_date = '2025-06-01';能看到是否命中索引。天气表数据量超过十万条时,不要在city_name上用like '%北京%',这类写法会让索引彻底失效,改成city_code精确匹配是更稳妥的方案。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询