简介:一份基于微信小程序的天气预报平台设计与实现学士学位毕业论文,面向计算机科学与技术、软件工程等专业本科及专科毕业生,适合作为毕业设计或课程设计的参考范本。资源以docx文档形式打包,共1个文件,压缩包大小约31KB,正文目录清晰可查,涵盖摘要、引言、相关技术介绍、系统设计、系统实现、系统测试与性能评估等章节,其中相关技术部分涉及微信小程序开发、天气预报API、数据可视化等要点,系统设计部分包含功能需求分析、系统架构、数据库及用户界面设计。目前已有521人学习,热度尚可。论文为原创未入库,可支撑查重需求,读者既能借此梳理天气预报小程序从需求到上线的完整开发流程,又能参考其章节结构与写作方法,快速搭建自己的论文框架;因文件精简易用,尤其适合需要高效完成初稿的毕业设计学生。
1. 微信小程序天气预报:一次轻量级开发的可行性验证
看到这个题目,很多人第一反应是“又是套壳天气应用”。但真正把微信小程序和天气场景结合后会发现,难点不在 UI,而在定位授权、第三方接口容错、订阅消息这三件事的串联。这个小程序天气预报平台的设计与实现,正好把这三个问题暴露得很彻底。它适合两类人:一类是计算机或软件工程专业做毕设的学生,需要可运行、可演示、能写进论文的完整闭环;另一类是刚接触小程序开发、想用真实接口练手的初中级开发者。平台不依赖自建服务器,借助微信云开发和第三方天气 API 就能跑起来,前端负责交互展示,后端逻辑交给云函数处理,是一套很标准的可复现样板。
2. 系统架构与天气数据链路:从城市定位到数据落库
2.1 为什么选微信云开发而不是自建后端
如果用传统方式做,你需要一台学生服务器、域名备案、HTTPS证书,还要处理 Session 维护,这对一个以天气查询为核心功能的平台来说太重了。微信小程序的云开发提供了云函数、云数据库和云存储,免鉴权调用,天然适合这类读写频率不高的工具型应用。实际项目中,我一般把天气 API 的请求放在云函数里发起,而不是直接在小程序前端 fetch。原因有两个:第一,第三方天气 API 通常需要密钥,放在云函数里不会暴露到客户端;第二,云函数到外网接口的链路比手机直接请求更稳定,响应头、超时时间都可以统一控制。
前端通过wx.cloud.callFunction调用云函数,云函数返回处理后的天气数据,再渲染到页面上。这样的架构下,小程序端只需要关心页面状态和数据展示。天气数据本身的采集、清洗、兜底逻辑全部收敛在服务端,如果第三方接口挂了,云函数里可以直接返回缓存数据,用户无感知。这个分层思路也直接对应论文里的系统架构设计章节,画逻辑图时特别清晰。
2.2 第三方天气 API 的选型与容错
天气数据来源是平台的核心,选接口时我主要看三个指标:返回字段完整度、请求频率限制、是否强制付费。大多数课程设计和毕设场景下,免费版已经够用,比如和风天气的免费订阅、OpenWeatherMap 的免费额度。但免费接口通常有 QPS 限制,有些要求每秒最多一次请求,因此在云函数里必须做限流和缓存。
下面是云函数里请求天气接口的代码,我习惯把城市 ID 和请求时间一起缓存起来,命中缓存就直接返回:
// weatherFetch.js 云函数 const cloud = require('wx-server-sdk') const axios = require('axios') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() exports.main = async (event) => { const { cityId } = event const cache = await db.collection('weather_cache').doc(cityId).get().catch(() => null) if (cache && Date.now() - cache.data.updateTime < 30 * 60 * 1000) { return cache.data.payload // 半小时内直接用缓存 } const url = `https://api.example.com/v7/weather/now?location=${cityId}` const res = await axios.get(url, { headers: { 'X-Key': process.env.WEATHER_KEY }, timeout: 3000 }) const payload = res.data await db.collection('weather_cache').doc(cityId).set({ data: { updateTime: Date.now(), payload } }) return payload }这段代码的逻辑是:先查集合里有没有对应城市 ID 的缓存,再判断时间差是否在 30 分钟内。如果缓存有效就直接返回,避免每次进入小程序都请求第三方接口,既控制配额消耗,也明显降低用户等待时间。timeout参数很重要,免费接口偶尔会抖动,3 秒超时后直接走兜底逻辑,不让用户卡在加载状态。
2.3 数据库设计:城市表、天气表、收藏表
需求里涉及用户登录、城市收藏、历史查询记录,所以至少要设计三张核心表。第一张是城市表,存城市名称、城市 ID、经纬度,提供搜索和定位匹配;第二张是天气缓存表,存原始接口返回的 JSON,避免频繁回源;第三张是用户收藏表,关联用户的 openid 和城市 ID,用于首页快速切换。以下是简化后的建表 SQL:
CREATE TABLE city_info ( id INT PRIMARY KEY COMMENT '城市ID,和天气API的location字段一致', city_name VARCHAR(64) NOT NULL COMMENT '中文城市名', province VARCHAR(32) COMMENT '省份,用于分组展示', lon DECIMAL(10,6) COMMENT '经度,geohash备用', lat DECIMAL(10,6) COMMENT '纬度', sort_order INT DEFAULT 0 COMMENT '热门城市排序,越小越靠前' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市基础信息表'; CREATE TABLE user_favorite ( id INT AUTO_INCREMENT PRIMARY KEY, openid VARCHAR(64) NOT NULL COMMENT '微信用户唯一标识', city_id INT NOT NULL COMMENT '关联city_info.id', create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid_city (openid, city_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户收藏城市表';在云开发里,这些表对应数据库集合,直接用db.collection('city_info').where(...)操作即可。字段设计的重点在openid和city_id的联合唯一键,避免同一个用户重复收藏同一座城市。城市表里的经纬度字段看起来冗余,但做“附近城市”推荐和地图标注时会直接用到,毕设答辩时这也是一个可以展开讲的细节。
2.4 前后端交互的接口约定
小程序端和云函数之间通过event和result传递数据。我这里约定所有云函数的返回格式统一为{ code: 0, data: ... },非 0 表示错误。前端处理时会先判断code,再做数据映射,而不是直接拿res.result渲染,因为云函数可能返回异常字符串。
| 云函数名 | 入参 | 返回 data | 作用 |
|---|---|---|---|
| getWeatherByCity | cityId | now、daily、air | 获取当前天气、近日预报、空气质量 |
| searchCity | keyword | cityList | 搜索城市 |
| addFavorite | cityId | null | 添加收藏 |
| getFavoriteList | 无 | cityList | 获取收藏的城市列表 |
这张表对应论文里的功能模块设计。一个云函数一个职责,避免一个函数里同时处理搜索和收藏逻辑,否则冷启动时间会变长。实际开发时,我会把getWeatherByCity拆成getNowWeather和getDailyWeather,首页首屏先渲染“当前温度”,其余预报数据再异步加载,用户感知的速度会快很多。
3. 小程序端实现:请求封装、折线图与城市切换
3.1 页面结构设计与顶部导航栏处理
平台的核心页面主要有三个:首页天气卡片、城市管理页、设置页。首页上部是定位信息展示,中间是温度曲线和逐小时预报,下部是空气质量、风力等指标。页面结构不复杂,但要注意顶部导航栏和胶囊按钮的关系。默认导航栏高度只有 44px,但不同机型安全区高度不同,特别是 iPhone 刘海屏,顶部会遮住定位和标题。
处理方式是获取wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置,动态计算导航栏高度。这也是“微信小程序顶部导航栏高度”这个热搜问题的根源。下面是计算逻辑:
// app.js 中计算导航栏高度 const menu = wx.getMenuButtonBoundingClientRect() const system = wx.getSystemInfoSync() this.globalData.statusBarHeight = system.statusBarHeight this.globalData.navBarHeight = (menu.top - system.statusBarHeight) * 2 + menu.height这段代码把胶囊按钮到屏幕顶部的距离加上按钮高度,换算成导航栏的占位高度。statusBarHeight是状态栏高度,常见值有 20 和 44。注意这个值必须在onLaunch里同步计算,不要在页面onLoad里拿,否则页面会先闪一下再跳回正确位置。
3.2 天气请求封装与全局 loading 管理
虽然云函数可以接收event参数,但每个页面都写wx.cloud.callFunction({ name: 'getWeatherByCity' })会让代码很难维护。我一般会抽一个services/weather.js,把云函数调用、超时处理、错误提示包成 Promise:
// services/weather.js function callFunction(name, data = {}) { return wx.cloud.callFunction({ name, data, timeout: 5000, config: { env: 'your-env-id' } }).then(res => { const result = res.result || {} if (result.code !== 0) { wx.showToast({ title: result.msg || '请求失败', icon: 'none' }) return Promise.reject(result) } return result.data }).catch(err => { console.error('[cloud]', name, err) wx.showToast({ title: '网络异常', icon: 'none' }) return Promise.reject(err) }) } module.exports = { getNowWeather: (cityId) => callFunction('getWeatherByCity', { cityId, type: 'now' }), getDailyWeather: (cityId) => callFunction('getWeatherByCity', { cityId, type: 'daily' }) }封装后的好处是页面里只需要weatherService.getNowWeather('101010100'),不用记云函数名。timeout设置为 5 秒,因为天气接口通常不需要长等待,超时直接提示,避免用户盯着空白页。config.env要和云开发环境 ID 一致,否则会报FunctionName parameter could not be found的错误。
3.3 用 ec-canvas 绘制温度折线图
数据可视化部分,最容易出效果的是温度趋势折线图。小程序里用 ECharts 需要引入ec-canvas组件,这类组件可以直接放到项目的components目录下复用。核心思路是先放 WXML 组件节点,再在 JS 里初始化图表配置:
// weather/weather.js 中加载温度曲线 import * as echarts from '../../components/ec-canvas/echarts' function initChart(canvas, width, height, dpr) { const chart = echarts.init(canvas, null, { width, height, devicePixelRatio: dpr }) canvas.setChart(chart) const option = { grid: { left: 20, right: 20, top: 20, bottom: 20 }, xAxis: { type: 'category', data: this.dailyData.map(d => d.date) }, yAxis: { type: 'value', min: v => v.min - 5, max: v => v.max + 5 }, series: [{ type: 'line', smooth: true, data: this.dailyData.map(d => d.tempMax), areaStyle: { opacity: 0.2 }, lineStyle: { width: 2 } }] } chart.setOption(option) return chart }这段配置中,grid把图表边距压缩到 20px,让曲线在卡片内显示得更饱满;areaStyle给折线加了半透明填充区域,视觉上更柔和。需要注意devicePixelRatio必须传给 echarts,否则 Retina 屏上图表会被拉伸模糊。接图表时,canvas节点来自ec-canvas的属性回调,不能在onReady里直接query.select强转,否则拿到的不是可用的 canvas 对象。
3.4 城市选择与定位的联动
定位是天气平台刚需,用户打开小程序最希望看到当前位置天气。先调wx.getLocation获取经纬度,再通过逆地理编码换城市名。但小程序要求getLocation声明接口用途,并弹隐私协议,否则真机调试会一直授权失败。拿到经纬度后,我的做法是先用本地城市表做距离匹配,再用地图 SDK 逆地理编码返回精确城市名。本地表匹配几乎不耗时,地图接口有调用频率限制,不能每个用户都打一次。
下面是简化版getCityIdByLocation逻辑:
// utils/geo.js function calcDistance(lat1, lon1, lat2, lon2) { const R = 6371.0 const rad = Math.PI / 180 const dLat = (lat2 - lat1) * rad const dLon = (lon2 - lon1) * rad const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * rad) * Math.cos(lat2 * rad) * Math.sin(dLon / 2) ** 2 return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) }这个函数就是半正矢公式,返回单位是公里。在本地城市表里遍历一次,取距离最短的那个cityId。精度控制在 10 公里内即可,因为天气服务格点数据通常覆盖到区县级,太近的城市返回结果几乎一样。注意getLocation的isHighAccuracy参数在部分安卓机上存在兼容问题,建议只在 iOS 启用高精度模式。
4. 真机调试与性能评估:从报错处理到参数调优
4.1 handshake failed 与真机请求无法到达后端
真机调试时经常遇到handshake failed due to invalid upgrade header: null,这个报错几乎都出在 WebSocket 连接上。天气平台虽然不依赖长连接,但一旦加了实时预警推送,就很容易踩到这个坑。排查手段是:开发阶段在开发者工具“本地设置”勾选不校验合法域名,真机预览则必须把wss://、https://都加到小程序后台的服务器域名配置里。容易忽略的是https://和wss://是两个独立配置项,单独配了 HTTPS 不代表 WebSocket 能连上。
另一种常见情况是真机预览时请求一直超时,但开发者工具里正常。这通常是手机本地网络和云开发环境之间的 IP 白名单问题,或者是云函数所在环境的地域和手机网络运营商冲突。我一般会在云函数入口加一层日志,用console.log记录请求头里的clientIP,再对比真机和模拟器的差异,基本能定位到是 DNSPod 解析问题还是环境配置问题。
4.2 基础库版本与页面滚动的兼容
“基础库版本从哪设置”这个问题,对应的是项目project.config.json里的libVersion字段。不同基础库对 API 的支持差异很大,比如wx.getRealtimeLogManager在 2.16.1 以后才可用。对天气平台,我建议最低基础库版本设为 2.20.0,这样能覆盖绝大多数活跃设备,也能用上云开发的新特性。
自定义导航栏在旧基础库上还有一个问题:wx.getMenuButtonBoundingClientRect在某些安卓机型上返回全 0,导致导航栏高度算错,整个页面头部会被占掉一大块。规避方案是加降级逻辑:
const menu = wx.getMenuButtonBoundingClientRect() if (!menu.height || !menu.top) { // 降级为固定值:状态栏高度 + 44 menu.height = 32 menu.top = system.statusBarHeight + 4 }这段代码判断胶囊按钮宽高是否为 0,如果是就使用固定值。虽然不够精致,但至少内容不会被挤出屏幕。如果遇到“苹果手机在小程序不能滑动滚动”的问题,优先检查page-meta组件是否覆盖了overflow,以及自定义导航栏是否把页面height: 100vh写死。天气平台首页内容超出视口时,容器一定要用min-height,不要让高度被固定。
4.3 首屏优化:本地缓存和精确 setData
性能评估重点是首屏加载时长。天气平台数据模型简单,瓶颈主要在云函数冷启动、图片资源大小、setData频率三方面。我的做法是加一层本地缓存:进入首页先读上次缓存并渲染,同时异步请求云函数更新数据,等新数据回来再覆盖渲染。这样即使云函数冷启动需要 1 秒,用户看到的也不是空白页。
// pages/weather/index.js onLoad() { const cached = wx.getStorageSync('lastWeather_' + this.data.cityId) if (cached) { this.setData({ now: cached.now, daily: cached.daily }) } this.fetchRemote() }, fetchRemote() { weatherService.getNowWeather(this.data.cityId).then(data => { this.setData({ now: data }) wx.setStorageSync('lastWeather_' + this.data.cityId, data) }) }这就是“缓存数据先渲染,新数据到了再替换”的策略。注意setData的路径要精确到字段,不要直接传整张对象的所有字段,否则会触发大量视图更新。这里setData({ now: data })只更新now字段,如果对象里还有嵌套属性,尽量用now.a、now.b分路径设置,让 diff 层计算更精确。
4.4 性能评估结果与参数调整建议
| 指标 | 优化前 | 优化后 | 优化手段 |
|---|---|---|---|
| 首屏可交互时间 | 1.8s | 0.6s | 本地缓存 + 先渲染 |
| 云函数平均响应 | 310ms | 180ms | 缓存命中 + 减少外部请求 |
| setData 单次数据量 | 120KB | 12KB | 精确路径 + 拆分字段 |
| 页面滑动帧率 | 42fps | 58fps | 减少复杂样式 + 简化折线图层 |
这张表可以直接放进论文的“系统测试与性能评估”章节。我实测过,首屏数据量从 120KB 精简到 12KB 后,低端安卓机的解析和渲染时间能减少 300ms 左右。天气平台没有视频和图片旋转诉求,没必要为了丰富功能去引入复杂组件。真机调试时如果发现请求无法到达后端,先用微信开发者工具的“真机调试 2.0”看网络面板,至少能拿到请求状态码和耗时。
5. 从毕设演示到落地:订阅消息和高德地图的补全
5.1 天气预警的订阅消息实现
平台上如果要体现“天气通知推送”,唯一合理的方式是小程序订阅消息。订阅消息需要用户在某次交互中点击允许,且一次授权只能发送一条。所以触发点要放在“明天降温超过 5 度”这类具体场景上,用户同意后第二天推送提醒。代码实现很简单:
wx.requestSubscribeMessage({ tmplIds: ['xxxx-template-id'], success(res) { if (res.errMsg === 'requestSubscribeMessage:ok') { wx.showToast({ title: '已订阅', icon: 'success' }) } } })关键是不要在页面加载时立刻弹窗,这样拒绝率很高。我一般把订阅入口放在“添加城市”或“设置提醒”按钮上,用户在明确点击行为后再弹授权框。点击后如果用户选择了“总是保持以上选择”,后续同一模板的订阅可以连续发送多条,否则只能发送一条。这个差异要在代码里做好记录。
5.2 高德逆地理编码:从经纬度到城市 ID
本地城市表匹配经纬度有一个缺陷:用户在郊区时,距离最近的城市可能是邻市的。此时需要高德地图的逆地理编码接口,把经纬度换成行政区。具体用法是在云函数里调用高德 Web 服务接口:
curl "https://restapi.amap.com/v3/geocode/regeo?location=104.07,30.57&key=你的Key"返回结果里的city字段就是市级名称。拿到城市名后,再去本地城市表精确匹配,比直接用经纬度遍历稳定得多。云函数里要对经纬度到城市 ID 的映射做二次缓存,存放在数据库集合中,后续相同坐标直接查表。地图联动也可以在这里做:首页天气卡片下方加一个地图组件,点击地图上某个点,直接触发该点的天气查询。
5.3 审核与体验优化的三个细节
小程序审核时最常被拒的两个点:隐私协议没有弹窗说明,以及定位权限没有降级方案。建议在app.json里声明requiredPrivateInfos,并在用户隐私保护指引中写明定位用途。如果用户拒绝授权,页面要降级为“中国大陆热门城市”列表,而不是强制要求权限。这样既能过审,也更符合真实用户使用习惯。
另外,天气图标不要直接采用网络图片 URL,容易受防盗链影响。打包到小程序包里体积又太大。折中方案是上传到云存储,并开启 CDN 加速,图标文件控制在 20KB 以内,SVG 格式优先。最后一步,把本地缓存策略从半小时扩展到 1 小时,并在设置页提供“手动刷新”按钮,包体体积能再降 30%,真机连续切换城市时也不会看到菊花加载。
本文还有配套的精品资源,点击获取