SpringBoot+微信小程序天气应用:从数据接入到智能推送的完整实战

发布时间:2026/10/17 16:34:08
SpringBoot+微信小程序天气应用:从数据接入到智能推送的完整实战
1. 不要急着写代码先把这个项目的边界划清楚每年到毕设季类似选题都会被反复拿出来做。很多人第一反应觉得天气类应用不是早就到处都是了吗再做有什么意义实际不是这样。这个题目真正想考察的不是你能显示多少天气数据而是你能不能把一个真实场景从前端到后端完整落地。尤其是“智能提示”这四个字决定了这个项目是停留在查询工具还是能上升到“服务”层面。我见过不少类似项目的初版方案功能清单拉得很长比如搞什么签到、积分、分享海报、社区发帖但真正核心的“天气查询”和“智能提示”反而做得非常薄。这是一个很常见的方向跑偏。天气类小程序的用户诉求说白了就三件事我今天出门穿什么、会不会下雨、天气有没有异常变化。把这三件小事做到位比堆砌十个花哨功能都有用。技术选型上前端选微信小程序自然是顺理成章的微信生态自带登录能力和订阅消息推送能力用户不用额外安装应用触达成本低。后端选 Java SpringBoot主要是工程化做得够好MyBatis-Plus、Redis、定时任务、统一异常处理这些都有非常成熟的解决方案。对毕设来说“功能闭环 技术覆盖面合理”比“用了冷门框架”要加分得多。所以我在设计这个系统时把整个项目分成了几条主线用户怎么登录、城市怎么管理、天气数据从哪里来、如何把天气数据转化成可执行的建议、以及如何按时把建议送到用户手机上。每一条线都不难但把它们串起来就是一个典型的“接口开发 数据缓存 定时任务 第三方对接 消息推送”的综合项目。1.1 功能清单怎么定才合理我把功能收敛成下面几个模块看起来不多但每个模块都有往下挖的空间实时天气查询支持按定位或者手动选择城市查看气温、天气现象、空气质量、风力、湿度、紫外线等。多城市管理用户可以把常关注的几个城市加进列表比如“工作城市 老家城市 旅游目的地”。智能提示规则根据气象条件自动生成穿衣、携带雨具、通勤、运动等建议。订阅消息提醒用户开启提醒后每天定时或触发极端天气时收到消息推送。基础用户体系基于微信登录建立用户保存个性化偏好。管理后台我暂时按最简单的来只做用户列表、规则阈值配置、推送记录查询。如果加得太重开发周期撑不住。做项目应该先做减法再做加法核心链路稳定了再谈扩展。1.2 智能提示并不是简单的 if-else很多人会把“智能提示”写成一串生硬的条件判断气温低于10度就提示“添加衣物”降水概率大于50%就提示“带伞”。这样写跑得起来但基本谈不上智能。稍微进阶一点要处理组合场景。比如气温 15 度、有小雨、风力 4 级单独看每一条规则都只能产生一条提示但组合起来应该是“降温降雨伴大风建议换上防风外套并携带雨具如果属于敏感人群则不建议户外晨练”。这要求规则引擎具备一定的优先级和多因子聚合能力而不是简单罗列。这部分是项目最容易出彩的地方也是绝大多数现成代码里最欠缺的地方。后面我会专门用一节讲规则引擎的设计这里先明确一个原则提示应该给用户带来确定性的“下一步动作”而不是复读天气指标。2. 架构拆分与数据库设计先把系统的骨架搭扎实2.1 前后端模块怎么划我个人习惯把工程先分三层分别是接入层、业务层和数据层避免所有逻辑堆在 Controller 里。接入层小程序端调用的 Controller负责参数校验、鉴权、统一响应格式。业务层用户服务、城市服务、天气服务、提醒服务、规则评估服务。数据层MySQL 存储业务数据Redis 做缓存和分布式锁。服务端划分上Application 启动类负责组件扫描和启动配置各模块按包名拆开例如 controller、service、mapper、entity、common。这个结构很常规但好维护。之前见过同学把整个项目代码塞进一个包下面后期要找一个小功能得翻半天包结构这种事还是要一开始就定好。2.2 MySQL 核心表结构设计先看建表。以下是我按实际业务整理出来的核心表字段已经精简过去掉了和场景无关的冗余字段。-- 用户表 CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL COMMENT 微信openid, nickname varchar(64) DEFAULT NULL, avatar_url varchar(255) DEFAULT NULL, prefer_city varchar(32) DEFAULT NULL COMMENT 默认城市code, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 关注城市表 CREATE TABLE t_user_city ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, city_code varchar(32) NOT NULL, sort_order int(11) DEFAULT 0, is_default tinyint(1) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_user_city (user_id, city_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 提醒偏好表 CREATE TABLE t_remind_rule ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, city_code varchar(32) NOT NULL, morning_time varchar(8) DEFAULT 07:30 COMMENT 每日定时提醒时间, rain_threshold int(11) DEFAULT 50 COMMENT 降水概率阈值, cold_threshold int(11) DEFAULT 10 COMMENT 低温阈值, enable_uv tinyint(1) DEFAULT 1, enable_air tinyint(1) DEFAULT 1, status tinyint(1) DEFAULT 1, PRIMARY KEY (id), KEY idx_user_city (user_id, city_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 推送记录表 CREATE TABLE t_push_log ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, city_code varchar(32) NOT NULL, message_type varchar(32) NOT NULL COMMENT daily/alert, content varchar(500) NOT NULL, push_status tinyint(1) DEFAULT 0 COMMENT 0未发1成功2失败, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几点经验openid 一定做唯一索引这是微信登录的核心标识。城市字段不建议存城市中文名存城市编码展示层再去映射否则城市改名或者同名的坑会让你很难受。提醒规则表单独建表存放而不是塞进用户表因为一个用户可能会设多个城市的提醒字段粒度要跟业务对齐。2.3 接口约定与统一响应前后端接口我用统一响应体包裹避免出现那种“有的接口直接返回对象、有的返回字符串、有的报错直接吐堆栈”的混乱情况。{ code: 0, msg: ok, data: {} }几个核心接口大概是这样的方法路径说明POST/api/user/login微信登录换取openidGET/api/weather/now实时天气GET/api/weather/forecast未来多天预报POST/api/city/add添加关注城市GET/api/remind/rule获取提醒偏好POST/api/remind/rule保存提醒偏好GET/api/push/log/page推送记录分页接口尽量保持在十个以内。模块之间通过 service 互相调用Controller 层只做参数接收和结果组装。这种约定在做联调的时候特别省心小程序端和后台各写各的只要响应结构固定问题就少一半。3. 天气数据接入第三方接口、缓存与容错3.1 数据源怎么接这部分是整个后端的数据源头。真实项目里一般会接入天气服务商的开放接口按城市编码或者经纬度去请求实时天气、逐日预报、空气质量、生活指数等。我在设计时做了一个统一的门面接口后续换数据商时只需要改一个实现。对外暴露的方法大概是这样的public interface WeatherSource { WeatherRealtime getRealtime(String cityCode); WeatherForecast getForecast(String cityCode, int days); }业务层只面向这个接口编程。接具体厂商的时候做成一个实现类负责拼参数、发请求、解析结果。要注意几个坑第一不同数据商的字段命名差异很大有的叫 condition有的叫 text有的返回中文天气现象有的返回英文代码解析层必须统一转换成项目内部的实体对象不要把第三方字段直接透传到前端。第二单位问题。温度和风速最容易出错有的返回摄氏度有的返回华氏度有的风力直接给字符串“3-4级”而有的只给风速数值。存到底层之前先做一次归一化。第三限流。免费档次的天气接口通常有 QPS 限制比如每秒一次或者每天一定次数。如果小程序端每个用户打开页面都实时请求第三方接口很容易被打回。解决思路是分两层一层是 Redis 缓存另一层是热点城市定时预取。下面是简化版接入封装核心逻辑就是请求、解析、归一化Service public class WeatherServiceImpl implements WeatherService { Autowired private WeatherSource weatherSource; Autowired private RedisCache redisCache; Override public WeatherRealtime getRealtime(String cityCode) { String key weather:realtime: cityCode; WeatherRealtime cached redisCache.get(key, WeatherRealtime.class); if (cached ! null) { return cached; } WeatherRealtime data weatherSource.getRealtime(cityCode); if (data ! null) { redisCache.set(key, data, 30, TimeUnit.MINUTES); } return data; } }TTL 设在 30 分钟比较合理天气数据在本小时内的变化有限用户感知不到和实时数据的差异。3.2 缓存穿透和击穿怎么防天气接口的缓存不能只做一个“查了再缓存”的简单逻辑还要考虑缓存穿透。如果用户手动输入一个不存在的城市编码后端每次都会打到第三方接口既浪费配额又慢。这里用一个空值缓存兜底查不到数据就缓存一个空标记。还有更严重的缓存击穿某个热门城市的 key 突然过期瞬间大量请求同时打到第三方直接把配额干穿。这种场景用一个简单的互斥锁就能挡住代码里用 Redis 的 SETNX 做占位拿到锁的请求去刷新缓存其他请求暂时返回旧数据或者等待。建议你在项目说明文档里把这一层讲清楚这属于非常典型的“看似简单、实际有坑”的项目加分点。3.3 定时任务与配额管理每日定时提醒需要一个调度器。我在 SpringBoot 里用自带的 Scheduled 就能满足不一定需要引入 Quartz。比如每天凌晨预取一批热门城市天气早上 7 点多执行用户提醒扫描。Component public class WeatherTask { Scheduled(cron 0 10 0 * * ?) public void preFetchHotCities() { ListString cityCodes cityMapper.selectHotCityCodes(); for (String cityCode : cityCodes) { try { weatherService.refreshCache(cityCode); } catch (Exception e) { log.warn(预取天气失败, cityCode{}, cityCode, e); } } } }这里有几个容易踩的坑。一是定时任务默认单线程执行如果有任务阻塞后续任务会排队所以耗时的任务单独配置线程池。二是服务器如果部署了多个实例多个实例同时跑定时任务会导致重复推送需要用一个分布式锁来保证同一个时刻只有一个实例在执行。三是对第三方接口的调用增加重试机制但重试要带退避不要每次失败后都在同一秒打过去。4. 智能提示规则引擎怎么写才能有“智能感”4.1 把规则抽象成可扩展的评估模型前面说过智能提示不能只是 if-else。我采用的方案是“规则项 影响等级”的模式。每个规则项包含触发条件、文案模板、影响等级。评估时逐条跑规则命中的规则按影响等级排序再合并成最终建议。这样新增一条规则不需要改核心代码只要在配置中心加一条记录即可。先看规则项的数据结构public class WeatherRule { private String ruleCode; private String name; private int impactLevel; private String condition; private String adviceTemplate; }条件部分用一个简单表达式解析。比如“precipProbability60 temperature15”表示降雨概率超过六成且气温低于15度。这样规则和代码解耦扩展起来非常舒服。再来看评估过程public ListAdvice evaluate(WeatherRealtime data, RemindRule userRule) { ListAdvice adviceList new ArrayList(); if (data.getPrecipProbability() userRule.getRainThreshold()) { adviceList.add(new Advice(rain, 3, 出门记得带伞雨天能见度也会下降)); } if (data.getTemperature() userRule.getColdThreshold()) { adviceList.add(new Advice(cold, 2, 温度低于阈值建议换上厚外套)); } if (data.getAqi() 100) { adviceList.add(new Advice(air, 2, 空气质量一般敏感人群减少外出)); } if (data.getUvIndex() 8 userRule.isEnableUv()) { adviceList.add(new Advice(uv, 1, 紫外线较强户外活动注意防晒)); } adviceList.sort(Comparator.comparingInt(Advice::getImpactLevel).reversed()); return adviceList; }这里如果引入规则引擎框架还能做到完全配置化。不过对于毕设来说把评估逻辑独立出来已经足够体现出可扩展性。想让项目更有说服力就把规则表设计出来并在文档里说清楚如何通过管理端新增一条规则而不用改代码。关键点在于同一时间命中多条规则时不要把所有话术都一股脑发送。比如已经提示“暴雨预警”了后面再跟着“穿衣指数”反而会稀释重点。所以要有一个聚合逻辑极端天气优先穿衣建议放到最后作为补充。4.2 文案模板怎么写才像人话提示文案的质量直接影响这个系统的观感。很多代码会写“温度10度请增加衣物”这种话术太生硬。让提示有人味文案里得包含场景和动作。降温场景“今天比昨天低了5度出门建议多加一件外套早晚温差明显。”降雨场景“午后有阵雨大概率持续到傍晚外出请带雨伞避开低洼路段。”大风场景“风力较大骑行的同学注意减速不要在广告牌附近逗留。”空气质量“今天空气不太好敏感人群建议戴上口罩减少长时间户外运动。”文案可以做成模板模板里带变量比如“今天比昨天低了{}度”。这就是规则表里 advice_template 字段存在的意义。4.3 用户个性化怎么落地个性化不是让用户自己写规则而是给用户提供几个开关和阈值输入。提醒时间用户选择早上的推送时间比如 7:00、7:30、8:00。关心类型勾选“降雨”“降温”“空气质量”“紫外线”等。阈值设置降水概率超过多少提醒低温低于多少提醒。城市绑定对不同的城市可以分别设置规则。后端保存这些偏好后在定时任务里按偏好拉取对应城市天气跑规则引擎生成文案后推送。做到这一层系统就不再是“千人一面”的纯查询工具而是有点“服务”的样子了。5. 小程序端定位、展示、订阅消息一个都不能少5.1 登录与定位授权小程序登录走 wx.login 拿到 code后端再调微信接口换取 openid。这个流程很成熟不再展开。比较复杂的是定位授权。用户点击天气页面的“定位查询”时需要先调 wx.getLocation。新版微信对个人隐私保护要求更高使用的时候要在 app.json 里配置 permission 说明并提前准备一个合理的解释文案。用户一旦拒绝授权千万别让页面卡死。我在页面里做了降级如果拿不到定位就展示默认城市天气并给一个“手动选择城市”的入口。wx.getLocation({ type: gcj02, success: (res) { const { latitude, longitude } res; wx.request({ url: ${apiBaseUrl}/api/weather/location, data: { latitude, longitude } }); }, fail: () { this.setData({ useLocation: false }); this.chooseCityManually(); } });后端收到经纬度后再把它转成城市编码。这一步可以由后端调用地理编码接口完成或者在小程序端先把经纬度通过逆地理编码转成城市名再传给后端。两种方式我都试过倾向于后端做因为城市编码的映射关系在后端更好维护。5.2 天气卡片的数据交互天气页面的核心是信息层级要清晰。我的页面结构分成三部分顶部是当前城市和切换入口中间是当前天气大图和实时温度下面才是空气质量、风力、湿度、紫外线等指标。注意不要试图把一屏塞满用户只看最重要的两三个信息就行。请求封装方面在小程序端统一封装一个 request 方法baseURL、header、token 都放在这里接口报错时统一 toast。function request(url, method, data) { return new Promise((resolve, reject) { wx.request({ url: API_BASE_URL url, method: method || GET, data: data || {}, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: () { wx.showToast({ title: 网络异常, icon: none }); reject(); } }); }); }页面上建议加下拉刷新和 loading 状态。天气页面是用户打开率最高的页面如果加载超过两秒体验就很差。后端已经把常用城市的天气缓存好了小程序端还可以再做一层本地缓存提升二次进入的打开速度。5.3 订阅消息的玩法与限制订阅消息是微信小程序实现“主动触达”用户的主要方式。原理是小程序端引导用户同意订阅后端调用接口下发消息。但有一个非常关键的限制需要提前了解清楚小程序里的订阅消息是一次性授权也就是说用户每次点击同意订阅服务端才获得一次下发的资格。这个限制经常让一些同学掉进坑里以为只要第一次同意以后都能发。所以设计上不能做“默认全员每天狂推”的方案。我采用的策略是把订阅时机放在有明确价值的位置比如用户开启某个城市的提醒、点击“明天上班前提醒我”这样的按钮时才弹出订阅请求。用户主动操作后的接受度远高于被动弹窗。每天定时推送时只给当天同意过订阅的用户发消息用户没有同意就记录为未推送等下一次交互时再引导。后端发订阅消息的关键点是 templateId 和页面跳转路径。模板消息的字段顺序要和订阅消息后台配置保持一致顺序错了会直接发送失败。内容也不要太长订阅消息的展现空间有限通常只放“城市一句话建议”就够。6. 部署联调与高频问题排查6.1 本地开发环境怎么配本地跑通这套系统大约需要准备这些环境JDK 8 或 17建议和 SpringBoot 版本匹配MySQL 5.7 以上建好数据库并初始化脚本Redis 6.x本地直接默认配置启动微信开发者工具导入小程序前端工程内网穿透工具用于调试需要回调的接口如果只是想验证主流程可以先关闭推送功能。对接第三方天气服务前先申请开发密钥并核对每日配额。我的建议是先在接口调试工具里把第三方接口调通确认返回结构再写接入代码否则会出现“代码写完了才发现字段名理解错”这种返工。存放密钥、数据库密码这些敏感配置不要直接写在代码里。工程里加一个 application-dev.yml 和 application-prod.yml 分开维护用环境变量或者配置中心注入。代码提交的时候注意不要把密钥提交到仓库里。6.2 典型问题与排查记录我整理一份自己在开发和联调时经常遇到的问题清单放在项目文档里会很有用。现象可能原因处理办法定时任务不执行启动类缺少 EnableScheduling在启动类或配置类上加注解推送发送失败提示模板ID无效小程序后台模板ID和代码不一致重新获取模板ID检查字段匹配天气数据一直是旧值Redis 缓存过期时间设置不合理检查 Redis key 和 TTL 设置同一用户被重复推送多实例跑定时任务没有加锁增加分布式锁或在配置中只保留单实例调度接口报 401登录 token 未传递或过期排查请求头和 token 刷新逻辑小程序拿不到定位未配置隐私协议或用户拒绝授权补充说明文案增加手动选择城市入口天气接口请求很慢第三方接口网络抖动增加超时配置用缓存兜底还有两个比较隐蔽的问题值得强调一个是“批量发送耗时过长”。定时任务扫描用户提醒时循环里挨个发订阅消息如果用户量大发送耗时特别长会影响下一个定时任务。我最后改成先扫描当日有资格推送的用户然后分批异步发送每批 50 个批与批之间留 200 毫秒间隔避免触发平台接口频率限制。另一个是时区问题。用户设置的提醒时间是“07:30”如果直接存字符串再比较是没问题的但如果用 timestamp 转成时间就会发现可能会因为服务器时区不同而差一个小时。我建议提醒时间用字符串或者 LocalTime 存储不要用时间戳去做刷新判断。6.3 再把“加分项”的路留好前面这些做完整个项目已经是一个功能闭环了。如果还有余力做增量可以考虑三个方向一是加一个简单的可视化统计页展示各城市天气情况、用户提醒触发次数等二是把规则引擎换成现成的轻量规则引擎算是技术亮点的延伸三是增加“恶劣天气主动告警”功能比如暴雨预警信号触发后立即推送而不等待每日定时。这三个方向都不影响主流程的稳定性又能展示自己的工程能力和思考深度。我见过最出彩的答辩就是这么做的不是堆功能而是把其中一条线挖得足够深深到能讲清楚设计动机和数据流。最后分享一点个人体会做这种题目最怕的不是技术难题而是做完之后自己讲不清楚“为什么要这么设计”。所以从第一天开始每写一个模块都要问自己一遍这个模块解决了什么问题、如果不做会怎样、有没有更省力的替代方案。把这些思考同步写进项目文档答辩的时候会轻松很多真正读你代码的人也会觉得这项目是靠思考堆出来的而不是靠复制粘贴堆出来的。回头再看这套系统的核心其实就是一条简单链路用户在小程序端提出需求后端拉取数据、缓存、分析、生成建议在有价值的时间点把结果推回给用户。把这个链路用 Java 工程化的方式做扎实不管是做毕设还是以后做真实业务系统都比单纯“会写接口”有价值得多。