
简介这是一套基于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.xCREATE 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT天气信息表;字段类型有几个值得注意的地方温度用decimal(5,2)足以覆盖从 -99.99 到 999.99 的范围避免使用float带来的精度漂移weather_date用date而不是datetime因为按日搜索时datetime会额外引入时间部分的匹配问题city_code单独存编码而city_name冗余一份是为了后续对接第三方天气API时能直接用城市编码做参数不用每次查表转换。2.2 SpringBoot与JPA的依赖配置项目选择Spring Data JPA而不是MyBatis原因是这个业务场景几乎全是单表操作插入一条天气记录、按日期查询列表、按主键修改或删除。JPA的派生查询方法在这种场景下比手写XML映射文件效率高很多。另外JPA的ddl-auto配置能根据实体类自动生成表结构对前期原型阶段特别友好。pom.xml里两个必要的依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意第二行新版SpringBoot 3.x中MySQL驱动坐标已经从mysql-connector-java改为mysql-connector-j旧坐标会导致依赖找不到或驱动类报错。application.yml配置如下spring: datasource: url: jdbc:mysql://localhost:3306/weather_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: falseddl-auto有五个可选值生产环境用validate只校验表结构不修改开发阶段用update可以在表不存在时自动建表create和create-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)这种映射关系比直接用Date和Double更严谨也避免精度转换的隐藏问题。Repository接口的写法是这个项目的精髓public interface TianqiInfoRepository extends JpaRepositoryTianqiInfo, Long { ListTianqiInfo findByWeatherDate(LocalDate weatherDate); OptionalTianqiInfo findByCityCodeAndWeatherDate(String cityCode, LocalDate weatherDate); Query(select t from TianqiInfo t where t.weatherDate between :start and :end order by t.weatherDate asc) ListTianqiInfo 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 ResultListTianqiInfo searchByDate(RequestParam(date) String date) { LocalDate queryDate LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE); ListTianqiInfo list repository.findByWeatherDate(queryDate); return Result.success(list); } }逻辑说明LocalDate.parse使用ISO标准格式yyyy-MM-dd解析字符串前端传2025-06-01没问题但如果传成2025/06/01会抛出DateTimeParseException。格式校验放后端的好处是任何客户端都得遵守同一规则不能只靠前端input[typedate]的约束。接口参数约定如下表参数名类型必填说明示例dateString是ISO格式日期范围 1900-01-01 以后2025-06-01返回结构统一为Result包装类包含code、message、data三个字段。成功时code200业务异常时返回code500和具体提示信息。统一包装的好处是前端处理逻辑简单只需要判断code是否为200不需要为每个接口单独处理异常形态。3.2 前端页面与JavaScript交互前台页面分两块区域搜索区放在页面上方天气结果列表和温度趋势图放在下方。搜索区用原生HTML搭配JavaScript的fetch完成数据请求不引入Vue或React这类框架。这是合理的因为页面状态很简单不需要双向绑定。div classsearch-bar input typedate idweatherDate value2025-06-01 button idsearchBtn查询天气/button /div div idweatherList/divdocument.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 classweather-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 ResultMapString, Object trend(RequestParam(days) int days) { LocalDate today LocalDate.now(); LocalDate start today.minusDays(days - 1); ListTianqiInfo list repository.findRange(start, today); ListString dates new ArrayList(); ListBigDecimal highTemps new ArrayList(); ListBigDecimal lowTemps new ArrayList(); for (TianqiInfo info : list) { dates.add(info.getWeatherDate().toString()); highTemps.add(info.getHighTemp()); lowTemps.add(info.getLowTemp()); } MapString, Object data new HashMap(); data.put(dates, dates); data.put(highTemps, highTemps); data.put(lowTemps, lowTemps); return Result.success(data); }前端ECharts配置最关键的点是xAxis的data必须与series的data下标一一对应否则折线图会出现错位或者断点const chart echarts.init(document.getElementById(trendChart)); const response await fetch(/api/tianqi/trend?days7).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; } }connectTimeout和readTimeout必须分开设置连接超时指建立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() { ListString 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?date2025-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精确匹配是更稳妥的方案。本文还有配套的精品资源点击获取