基于Spring Boot的天气预报管理系统:从架构设计到部署实践
发布时间:2026/10/11 9:15:46 锦皓数字建站

1. 为什么要做天气预报管理系统选题动机与需求边界1.1 “查天气”和“管理系统”之间的真实差距在开始前也许你会觉得“天气预报管理系统”这个名字又老气又简单。确实很多毕业设计排行榜上都有它的身影但正因为常见才更要做得有章法。我当时选这个题目主要是想验证自己能不能独立完成一个“从数据库到前端页面”的完整闭环。需求分析阶段我就发现如果只是做一个查天气的网页那不叫管理系统所谓“管理”必然涉及用户、城市、收藏、历史记录这些资源对象的维护。系统最终目标很明确普通用户能查询国内城市的实时天气并把自己关心的城市收藏起来形成“关注城市列表”管理员能维护城市基础数据管理注册用户保证数据的可用性。这个目标拆解之后开发范围一下子清晰了后续的表结构设计和接口设计都围绕这几条主线展开没有再出现“做到一半突然想加功能”的情况。我是从一次组会评审的教训里意识到这一点的——最初选题报告里我写了十几个功能点指导老师只问了一句“哪些是核心哪些是包装”我当场就没答好。后来把所有需求重新梳理成核心功能和扩展功能两个清单开发节奏才真正稳定下来。1.2 角色与用例三种身份的权限差异系统设计之初我把使用人群分成三类匿名访客、注册用户、系统管理员。匿名访客只能访问首页和天气搜索注册用户可以收藏城市、取消收藏、查看自己的查询记录管理员在用户基础上增加了对城市信息、用户状态、最近查询日志的维护权限。我给管理员加了一个“禁用用户”操作一旦用户被禁用登录接口会直接拒绝这个是实际运行中很有用的功能比如发现有人在刷接口管理员可以第一时间处理。权限控制在Spring Security里做得比较干净。配置类中定义了三层URL规则public路径放行user路径要求登录admin路径要求管理员角色。角色字段在用户表里用int存储0代表普通用户1代表管理员。演示时我专门用两个账号分别登录展示不同侧边栏菜单这比单纯在代码里写死权限说明更有说服力。角色设计虽然简单但涉及到一个常被忽略的点用户第一次注册时角色必须由后端固定设置为普通用户而不是从前端表单里读取否则一个普通用户可以通过篡改请求把自己变成管理员。我在代码里是直接在注册Service中写死user.setRole(0)前端的角色下拉框只会在管理员编辑页出现。1.3 需求边界哪些功能我选择不做必须强调完成一个系统不代表要把能联想到的功能全部做出来。我最初想加“未来一周天气预报”后来发现免费接口的7日数据在某些天气现象的字段返回上不一致需要大量兼容逻辑于是第一期先不做只做当天实时天气和24小时温度曲线。后来实在想做温度曲线才用ECharts补齐了24小时预报展示。另一个砍掉的是“天气预警地图”这个需要GIS支持复杂度远超毕设范畴。我给自己定了一个原则凡是和核心业务关系弱、开发成本高的功能一律延后或不做。事实证明这个“边界意识”在后期帮了大忙项目没有失控答辩时老师也更关注系统本身是否完整落地而不会因为你没做全能而扣分。需求边界一旦确定我就在项目文档里写了一个“不在本期范围内”的清单后面所有新增想法都先扔到这个清单里等核心功能全部稳定再去评估。没有这个清单我大概率会把项目做成一个充满半成品模块的大杂烩。2. 技术栈选型为什么只用Spring Boot也能撑起整个系统2.1 “Java Web”和“Spring Boot”到底是什么关系标题里“基于Java Web”这个表述很多同学会纠结Java Web是不是要写Servlet和JSP要不要用SSH其实不是。Java Web是一个大的技术方向而Spring Boot是目前最适合快速开发Java Web应用的框架之一。我选择它是因为在满足“基于Java Web”的前提下开发效率最高。Spring Boot对传统SSH或者SSM的整合做了大量自动配置。例如连接数据库时以前需要在applicationContext.xml里配置DataSource、SqlSessionFactory、MapperScanner而Spring Boot只需要在pom文件里引入starter并在application.yml中填写url、username、password。这种自动化程度并不意味着你可以不懂底层——恰恰相反我建议花时间看一看自动配置源码至少要知道它背后替我们做了哪几件事。印象最深的是内嵌Tomcat。Spring Boot的应用采用了独立可执行的jar包模式启动类main方法里会初始化整个应用上下文再启动内嵌的Tomcat容器。这意味着我不需要再安装单独Tomcat不需要处理war包放在webapps目录下的路径问题本地开发和服务器部署的体验几乎一致。这种一致性在后期调试时尤其重要因为很多bug只在特定环境触发使用内嵌容器能减少环境差异带来的干扰。如果你打算深挖Java Web原理我建议还是补一下Servlet生命周期、过滤器、监听器这些基础知识Spring Boot底层再自动核心机制依然沿用了Java Web标准。2.2 数据库选型MySQL做核心存储Redis做缓存补充系统的数据量不大但表关系并不少。用户表sys_user、城市表city、用户收藏表user_favorite、查询历史表search_history是四个核心表。用户和城市是多对多关系通过收藏表关联查询历史表与用户是一对多关系。MySQL是关系型数据库的标准选择表结构稳定事务能力可靠。注册用户、收藏城市这些写入操作都涉及数据一致性用MySQL的InnoDB引擎保证原子性非常合适。Redis则用来缓解第三方天气接口的调用压力。我把每次从天气API拿到的数据按“weather:{cityCode}”的key缓存过期时间2小时。用户重复查询同一城市时直接从Redis返回延迟从原来的1秒以上降到几十毫秒。有一点要提醒不要为了用Redis而用Redis。像用户信息这种低频修改、低频查询的数据MySQL加索引就够了。我前期过度设计了缓存后来又把很多缓存删掉了只保留对天气数据这种高成本数据源的缓存效果反而更好。选择缓存对象要基于成本估算不是所有数据都值得放Redis。2.3 Thymeleaf模板渲染一个人开发的小型Web项目最优解现在流行的前后端分离模式我非常认同但也要看场景。这套天气系统前端并没有复杂的表格化交互核心页面只有首页、天气详情、收藏列表、后台用户管理用服务端渲染足够而且可以少写很多接口。我选择Thymeleaf与Spring Boot天然契合只需要在controller里返回视图名模板文件里用th:each、th:text处理数据。服务端渲染的一个好处是用户看到的是完整HTML不需要等待Ajax请求循环加载另一个好处是权限数据可以直接在模板里渲染例如管理员菜单只有当前用户角色是管理员时才显示。部署时也简单jar包启动后页面都在内没有跨域和静态资源路径配置的烦恼。当然如果你本身就想练手前后端分离Vue可以照用Spring Boot只需要再加一层REST接口。实际上我在后期也加入了几个Ajax接口用于改善收藏和取消收藏的交互但核心页面还是服务端渲染这样复杂度可控代码也更容易维护。选型的关键不是追新而是想清楚自己的项目规模和维护成本。3. 核心功能实现从注册登录到天气查询的完整链路3.1 Spring Security BCrypt注册登录模块怎么落地用户认证我几乎没有手写Session管理直接用了Spring Security。这么做的好处是登录、会话、角色的底层逻辑都是现成的出问题的概率远比自己写拦截器低。关键实现有三个部分。第一UserDetailsService实现。通过用户名在sys_user表查询用户如果不存在抛出UsernameNotFoundException存在则组装成Spring Security的User对象同时把角色信息一并塞进authorities。第二密码加密。注册时使用BCryptPasswordEncoder的encode方法加密登录校验交给Spring Security自动完成。我不建议再用MD5泄库后字典破解太容易了。第三登录成功处理。自定义AuthenticationSuccessHandler重写onAuthenticationSuccess方法根据登录前的访问地址做跳转如果是管理员就跳到/admin/index普通用户跳到/user/home。有一个配置小坑Spring Security默认会拦截POST请求进行CSRF校验如果前端没有携带token登录会一直失败。我为了简化演示在配置中临时关闭了csrf等到真实部署前又重新开启并加上token参数这个开关一定要记得。后来我把CSRF token通过meta标签放到页面head里前端每个Ajax请求都会拿到这个token并放在请求头中这样既保留安全防护又不影响用户体验。调整完以后登录失败率显著降低因为很多登录问题根本不是密码错误而是CSRF校验拦截。3.2 城市搜索与天气查询Service层三级缓存设计城市搜索的核心不是模糊匹配算法而是把用户输入和标准城市编码对应起来。我维护了一张city表包含城市编码、城市名称、省份、拼音首字母。用户输入“北京”或“beijing”或“bj”后端通过统一规范匹配到同一条记录。我在Service里写了一个标准化方法先trim去除首尾空格然后判断是否全中文、全拼音还是英文缩写分别走不同查询逻辑。这个标准化逻辑虽然不复杂但显著提升了搜索体验。天气查询接口设计为GET /weather/query请求参数cityName。Controller层只做参数校验真正的业务逻辑在WeatherServiceImpl中。这个Service的查询逻辑比较关键我把它设计为三级第一级查询Redis。key为“weather:{cityCode}”命中直接返回第二级查询本地数据库的daily_weather表这张表每天由定时任务预写当天常用城市数据第三级调用第三方天气API拿到数据后异步回写Redis和数据库。三级顺序设计有一个隐含目标最大程度减少对外部API的依赖。有一次我在演示时网络断了因为前两级缓存的存在页面仍然能显示昨天的天气数据老师并没有察觉接口挂了这种容错能力比多写几个接口更体现系统质量。边界情况也要考虑如果第三级接口调用失败第二级数据已经超过24小时此时不能直接把陈旧数据返回要在页面顶部用一个小横幅提示“数据更新时间较早”避免误导用户。3.3 收藏、取消收藏与查询历史的实现细节收藏功能有明确的业务约束同一用户不能重复收藏同一城市。所以在UserFavoriteServiceImpl中我先通过userId和cityCode的联合查询判断记录是否已存在存在则直接返回“已收藏”的提示不存在才插入新记录。插入操作在Mapper里写insert语句时用ON DUPLICATE KEY UPDATE也能做幂等处理不过代码可读性稍微差一点我选择了先查后插。查询历史是一个低频写入、频繁删除的场景我用Async独立线程池处理。线程池参数设置为核心线程5最大线程10队列100。写入历史表时只记录userId、cityName、createTime、status不存冗长的天气JSON避免历史表膨胀。这里我一开始犯过分不清“查询历史”和“浏览日志”的问题后来把表字段精简为“用户城市时间状态”数据库只用了不到1MB性能完全够用。收藏列表页是所有功能里最“亮眼”的部分它会把所有收藏城市的最新天气也查出来按温度升序排列。用户一打开就能看到哪个城市冷哪个城市热这是我用收藏表join天气缓存表实现的逻辑并不复杂但演示时效果很直观。为了不让收藏列表的查询太慢我会在Service中调用一个批量查询方法一次从Redis拿多个城市的天气数据而不是循环调用单查接口这样避免N1查询问题。4. 天气数据接入第三方API的选型、对接与降级4.1 对比三家天气API为什么选了和风天气天气数据不可能自己造一定得用第三方接口。我在选型时把几家常被提到了API全注册下来试用了一遍包括和风天气、高德开放平台、聚合数据。高德平台本身不是专业天气服务商它提供的天气信息比较粗适合地图应用做辅助展示不适合作为主数据源。聚合数据的价格和返还结构不稳定我测试时发现相同接口在不同时间段返回字段不一致解析代码很难写稳。和风天气的专业性最强文档里有完整的数据结构说明和示例JSON免费开发版对个人开发者非常友好。当然使用任何第三方平台前都要细读服务条款比如是否允许商业用途、请求频率限制是多少、token过期怎么处理等。我申请的和风key有效期是三个月这意味着系统长时间不维护会失效所以我在管理后台里留了一个“天气数据源配置”页面管理员可以随时更新AppKey不需要重新发版。这个页面虽然简单但让我在答辩演示时不用修改代码就能换Key节省了大量临时处理时间。4.2 RestTemplate调用与JSON逐层解析的踩坑经验Spring Boot中的RestTemplate在调用外网接口时默认使用的是JDK的HttpURLConnection性能一般但够用。我在启动类里手动创建了一个RestTemplate Bean并设置了连接超时时间5秒、读取超时时间10秒。超时时间如果不设一旦对方服务无响应请求线程会长时间挂起很快拖垮Tomcat的默认线程池。当时我并没有意识到线程池会被耗尽直到有一次第三方接口故障整个系统所有请求都排队超时我才看到日志里堆满了pool-1-thread超时的异常。和风天气返回的JSON结构层级比较深比如当前天气的路径为now.temp24小时预报列表在hourly数组中。我最初尝试用对象直接映射定义一个WeatherResponse类里面用JsonIgnoreProperties忽略未知字段但后来发现接口升级会加字段对象映射很容易出现反序列化异常。最终改成用JsonNode逐层解析手动封装成自己的WeatherInfo对象。代码确实冗余了些但胜在稳定对方加字段不影响我。有人可能会说“这不够面向对象”但外部数据接口本身就是多变的用JsonNode做一次“防腐层”把第三方数据适配成领域模型其实是更稳妥的做法。4.3 接口不可用情况下的降级与重试策略第三方服务不可用是必然会发生的事我在一个下午就用完了免费版当日额度的体验想想还挺尴尬。从那以后我加入了三个保护层。第一层Redis缓存兜底。只要key没过期就优先用缓存哪怕第三方接口已不可用用户仍然能拿到上一次成功的数据。第二层Spring Retry做重试。我在调用接口的方法上标记Retryable(maxAttempts 2, backoff Backoff(delay 500))第一次失败后间隔500毫秒再试一次。第三层全局降级。如果重试仍失败返回一个构造好的默认天气对象页面显示“天气服务暂时不可用”同时把异常信息写入error.log方便后续排查。重试策略要克制重试次数太多反而会让对方服务雪崩。本场景下2次足够而且只对“查询”操作做重试对“收藏”“注册”等写操作一概不做避免重复提交。在降级返回默认天气对象时我会在城市名后面加上“(缓存)”字样提醒当前数据不是实时数据。这个设计非常人性化用户不会误以为系统是错的。如果你还需要更完善的降级可以参考Sentinel或Resilience4j但对于这个体量的系统Spring Retry加Redis缓存已经完全够用。5. 安全与性能毕设系统不能只追求“能跑”5.1 XSS过滤器与SQL注入防护的落地方式任何有输入框的系统都逃不开XSS和SQL注入这两个老话题。我用一个自定义过滤器处理XSS继承OncePerRequestFilter在doFilterInternal中重写请求参数把参数值里的替换为替换为再传入后续链。需要注意重写的对象必须是HttpServletRequestWrapper的一个实例因为原始request的参数Map不允许修改。SQL注入方面在MyBatis的Mapper里写SQL时必须使用#{}占位符。包括order by后面的字段名也不能直接拼接我最开始犯过这个错误根据传入的sortField动态拼orderBy结果传一个“id desc; drop table”就能把系统打崩。后来改成白名单校验只有允许的字段和排序方向才映射到固定SQL片段其他一律给默认值。设计白名单其实很简单一个Map就能搞定却能把最危险的动态排序、动态查询条件限死。5.2 缓存策略省着点用第三方接口额度天气接口免费额度有限如果一天要支撑几百个演示用户必须对请求量做预算。我在Service层统计过如果没有缓存策略同一个城市被10个人查一次就要消耗10次调用加上缓存后10个人的第2次查询全都命中本地只有第1次请求会打到第三方。仅这一项改动接口消耗就下降了约90%。同时在Redis过期时间设计上为了不让缓存太旧过期时间设置为2小时而本地daily_weather表则每天凌晨3点预生成配合一次天气接口调用。要注意本地表的数据并不可靠它只是降级时的一层保障判断数据新鲜度要靠字段updateTime超过24小时就不在业务层展示。这个缓存策略还可以更细比如针对“今天”的天气数据缓存过期时间设为2小时针对“24小时预报”数据设为30分钟因为预报数据更新频率更低放长一点没问题。5.3 定时任务、线程池参数与资源消耗控制Spring的Scheduled任务在只有一个实例时默认是单线程的。我项目中有两个定时任务“刷新天气数据”和“清理过期查询记录”如果都走默认调度器第二个任务必须等第一个执行完才会执行排队久了可能错过执行周期。我在配置类中创建了一个稍大一点的ScheduledExecutorService初始大小为2并把两个任务分别设置独立的固定延迟避免互相影响。查询历史的异步写入也使用线程池队列如果满了调用线程会被阻塞还是抛出异常这个要提前定好。我采用ThreadPoolExecutor.CallerRunsPolicy当队列满了任务会退回调用线程执行这样不会丢失用户操作记录只是逻辑上会给当前实时请求增加一点点耗时。考虑到历史记录不是核心链路这个策略是可以接受的。使用线程池最重要的参数不是大小而是饱和策略AbortPolicy会直接抛异常DiscardPolicy会静默丢数据CallerRunsPolicy在业务上往往更可靠。6. 打包部署与实测观察从本地到云服务器的完整记录6.1 Maven多环境配置与jar打包避坑部署前我在application.yml里增加了多环境配置。开发环境连接本地MySQL关闭缓存生产环境连接云数据库开启Redis缓存并把接口日志等级调成WARN。Maven通过profile控制激活哪个环境打包命令是mvn clean package -DskipTests -Pprod如果去掉-DskipTests测试类里的环境变量可能会干扰打包而-Pprod参数会在打包时把spring.profiles.active写进配置这样部署时不需要额外再传参。还有一个非常关键的细节如果服务器上JDK版本是8而开发环境用了JDK11的语法特性jar包运行时会直接报UnsupportedClassVersionError。我吃过这个亏后来在pom.xml里显式指定java.version为1.8同时把maven.compiler.source和target都设置为1.8确保编译产物和服务器运行环境完全一致。部署前最好先用java -jar试运行一下直接看启动日志里的Spring Boot版本和JDK版本是否匹配。6.2 Linux云服务器部署从安装JDK到Nginx反向代理我用的服务器是1核2G内存的云主机操作系统是Linux。部署流程大致是安装OpenJDK8和MySQL8创建数据库与专用账号将打好的jar包上传到/opt/weather目录使用nohup启动服务并把日志输出到同目录下的application.log检查进程和端口确认应用启动成功在Nginx配置中将域名或IP的80端口转发到本机8080端口。Nginx我还配置了对静态资源的缓存location ~* .(js|css|png|jpg)$ { expires 5d; }这样每次页面刷新不会重新加载大文件。反向代理的client_max_body_size我调到了10m虽然项目没有大文件上传但提前调大一些可以避免某些请求因体积被拒。启动方式我写了两个脚本start.sh负责启动jar并记录PIDstop.sh负责杀掉进程避免每次都得ps查PID再kill这个小工具在调试时省了不少时间。6.3 上线一周后遇到的两个典型问题系统上线后我连续观察了几天日志发现两个值得记录的问题。一个是首次访问某些城市的天气详情时耗时接近2秒这是因为首次查询没有缓存需要去外部API取数据。解决办法是我在Redis预热的定时任务里把访问频率最高的十个城市提前加载进本地天气表查询第一个城市也能秒开。另一个是Redis服务突然报错发现公网默认端口6379暴露被自动化脚本扫描并尝试写入垃圾数据。我在云服务器的安全组里限制了6379只允许内网访问并给Redis设置了强密码同时在Redis配置中把protected-mode设为yes。这件事之后我对“部署安全”有了真正的敬畏毕设项目一个漏洞不补服务器就真的可能变成别人的肉机。最后再提一个小经验在Controller层对城市名参数做一次trim和标准化能把很多隐藏问题挡在门外。我当时就因为参数多了个空格查不到数据排查了半天。这些小细节往往比炫酷技术更值钱。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。