1. 从零搭建天气系统:为什么 Python + Vue 是最省心的组合
先聊点实际的。很多人一看到“天气预报管理系统”这六个字,下意识就想到“调用个免费天气 API,把 JSON 解析一下,渲染到页面上,收工”。如果只是做一个一次性 Demo,这么干确实没问题。但如果你要交课程设计、做毕设、应付公司内部运维展示,或者准备一份能放进作品集的项目,那需求就完全不一样了。你需要的是一个“管理系统”,意味着要有用户、要有登录、要有城市管理、要有历史数据、要有可视化图表,甚至要有定时更新任务。这时候,Python + Vue 这套前后端分离的组合,几乎是个人开发者和学生团队最不吃力又能出效果的选择。
先说后端。Python 写后端服务的生态太成熟了,一个 Flask 或者 Django 就能撑起整个系统的 API 层,配合requests库去请求第三方天气接口,再用apscheduler做定时轮询,几乎没有难啃的骨头。而Vue负责前端展示,组件化开发方式让“天气卡片”“趋势折线图”“城市下拉选择器”这些模块各自独立,改起来不牵连全局。更重要的是,前后端分离之后,你调试接口、替换数据源、后期加功能都特别顺手——前端只管展示,后端只管提供数据,谁也不用拖着谁。
选型之外,还有一个容易被忽略的点:数据从哪来。网上免费天气接口很多,但有的收费、有的限流、有的返回字段乱七八糟,还有的动不动就失效。我的建议是,开发阶段先找一个稳定的免费源(比如国外的 OpenWeatherMap 或者国内的一些开放平台)把整个流程跑通,后期如果要做正式项目再考虑付费精度更高的数据源。这样既能保证你核心代码不依赖某个特定服务商,又不会因为 API 密钥过期导致整个项目瘫痪。
另外,这套组合的学习曲线也很友好。Vue 对新手来说,比 React 更好上手,模板语法直观,双向绑定省了很多 DOM 操作;Python 后端更是出了名的“伪代码级”可读性。你不需要是资深全栈工程师,只要能掌握 Flask 的路由写法、Vue 的组件通信和 axios 的调用方式,就足够完成一套拿得出手的系统了。今天这篇文章,我就把整个系统的开发思路、关键模块、踩坑点和扩展方向全部摊开讲,保证你照着走能跑通,也能理解你写出来的每一行代码到底在干什么。
2. 需求拆解:天气预报“管理系统”到底管什么
很多人在动手之前不清不楚,上来就写代码,最后做出来的东西既不像“预报”也不像“管理”。我们先花几分钟把需求梳理清楚,这比写代码更重要。
2.1 用户侧功能:查询、展示、收藏一个都不能少
一个天气预报管理系统,首先得解决“查天气”这个核心诉求。但你想想,查完之后呢?用户可能想收藏几个常关注的城市,下次打开直接看;可能想对比一下未来一周温度走势;可能想把某个城市的天气信息记录下来做个备注。这些需求虽然简单,但如果全都塞进一个页面,界面会乱成一锅粥。
所以我建议把用户侧功能拆成四块:
- 实时查询:输入城市名,展示当前温度、天气状况、风力、湿度、空气质量,精确到小时级更新。
- 多城市收藏:用户把常用城市加入收藏夹,形成“我关注的城市”列表,点击即切换,不用反复输入。
- 天气趋势分析:用折线图展示未来一周或过去七天的温度、湿度变化轨迹,视觉上直观,代码上也锻炼你对 ECharts 或 Chart.js 的掌握。
- 历史记录:后端数据库存下每次查询记录,用户可以翻看自己查过哪些城市,什么时候查的,顺便为后面的“热门城市统计”提供数据基础。
这四块需求三三俩俩地组合,就足以把前后端的增删改查全部覆盖到位,同时也不会因为需求太大导致做不完。
2.2 管理侧功能:用户、城市、配置都要有抓手
既然叫管理系统,那必然要有管理员视角。我见过太多人只做好了用户页面,定死“管理”两个字全靠登录时的角色标识,后台啥都没有,这是不行的。
管理侧至少要包含以下内容:
- 用户管理:查看注册用户列表、禁用异常账号、重置密码,这需要后端设计用户角色权限。
- 城市维护:预置常用城市列表,管理员可以手动添加或删除城市,保证前端城市选择器的数据是可控的。
- 天气数据配置:管理第三方 API 的 Key、更新频率、数据源地址,这些配置如果直接写死在前端是灾难级的错误——应该放在后端配置表里。
- 数据看板:展示系统里累计查询了多少次、哪些城市最热门、最近一次 API 抓取是否成功。简单几个统计数字,就能让项目从“功能演示”升维到“可视化管理”。
2.3 核心流程与技术决策
把功能理清楚之后,系统的核心流程也就浮出水面了:
- 用户在前端输入城市名,前端校验格式后调后端接口。
- 后端拿到城市名后,先去本地数据库/缓存表查是否有最近两小时内的天气数据。
- 如果有,直接返回;如果没有,后端调第三方天气 API,把返回数据落库,同时返回给前端。
- 前端展示数据,用户如果点击收藏,就把城市写入收藏表。
- 系统后台定时任务每隔半小时,自动把所有“收藏数大于0”的城市天气刷新一遍。
这个流程里藏着两个关键决策。第一,缓存优先:天气数据不是每次都要实时请求第三方,否则不仅慢,还容易触发对方限流,两小时的缓存策略是平衡实时性和稳定性最稳妥的方式。第二,落库留存:所有查过的数据都存进数据库里,为后面的趋势图和热门统计积累素材,也让你的系统有“数据沉淀”的感觉,而不是一个只起中转作用的壳。
3. 后端设计:Flask 路由、三级缓存和数据库模型
后端我用的是 Flask,理由很简单:轻量、灵活、两周左右就能跑完一个完整项目,不像 Django 那样自带一堆你可能用不到的组件。如果你的需求更复杂,要带 Admin 后台、要自带 ORM 管理界面,选 Django 也没错。我这套思路换到 Django 上一样适用,只是路由和配置有点差异。
3.1 数据库模型的三个表设计
数据库我用了 SQLite 起步,因为零配置、文件即库,个人项目和数据量不大时非常方便。正规部署时可以平滑迁移到 MySQL,SQLAlchemy 对底层的抽象保证了这一步不会太痛苦。表结构设计如下:
用户表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键,自增 |
| username | String | 用户名,唯一索引 |
| password_hash | String | 密码哈希值,绝对不存明文 |
| role | String | 用户角色:normal / admin |
| created_at | DateTime | 注册时间 |
城市收藏表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键 |
| user_id | Integer | 关联用户 ID |
| city_name | String | 城市名 |
| is_active | Boolean | 是否启用 |
| created_at | DateTime | 添加时间 |
这里要注意:user_id + city_name两者要加一个联合唯一索引,防止用户重复收藏同一个城市。
天气数据表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | Integer | 主键 |
| city_name | String | 城市名 |
| weather_json | Text | 原始天气数据 JSON 字符串 |
| fetch_time | DateTime | 抓取时间 |
| source | String | 数据来源标识 |
天气数据表只做两件事:缓存和留痕。weather_json字段存的就是第三方 API 返回的完整 JSON,我用 Text 类型而不是拆成无数个字段,就是因为第三方返回的数据结构千奇百怪,拆字段反而给自己找麻烦。你需要哪几个字段,在用的时候再解析就行。
3.2 三级缓存逻辑:内存、数据库、第三方接口
缓存这件事值得多聊两句,因为它直接决定了你的系统在演示时是“秒开体验”还是“转圈十分钟”。
我设计的三级缓存思路如下:
- 内存缓存:用一个 Python 字典存
{city_name: {data, expire_time}},设置两小时过期。命中内存缓存时,接口响应基本在 10ms 级别,体验最好。 - 数据库缓存:内存没有命中时,查数据库最近一条记录,如果抓取时间在 2 小时内,直接把
weather_json返回并重建内存缓存。 - 第三方 API:数据库也没有命中,才真正去请求第三方接口,请求成功后落库、更新内存,再返回给前端。
这三级的核心原则就一句话:能用缓存解决的,绝不轻易打第三方接口。一个“南京”早上查了 5 次,如果每次都去请求外部接口,第 5 次大概率会撞上流量限制,而且毫无意义,反正 2 小时内的天气不会产生奇迹般的变化。
实现上,我写了一个简单的WeatherService类:
import time import json import requests from datetime import datetime, timedelta # 内存缓存字典 memory_cache = {} class WeatherService: CACHE_EXPIRE_SECONDS = 7200 # 2小时 def get_weather(self, city_name): # 1. 尝试内存缓存 cached = self._get_memory_cache(city_name) if cached: return cached # 2. 尝试数据库缓存 cached = self._get_db_cache(city_name) if cached: self._set_memory_cache(city_name, cached) return cached # 3. 请求第三方接口 data = self._fetch_from_api(city_name) self._save_to_db(city_name, data) self._set_memory_cache(city_name, data) return data def _get_memory_cache(self, city_name): item = memory_cache.get(city_name) if item and item["expire_at"] > time.time(): return item["data"] return None def _set_memory_cache(self, city_name, data): memory_cache[city_name] = { "data": data, "expire_at": time.time() + self.CACHE_EXPIRE_SECONDS }3.3 定时刷新任务:让收藏城市永不掉线
系统里还有一个坑很多人想不到:用户收藏了一个城市,但如果你不主动去刷新收藏城市的天气,收藏列表永远是用户第一次收藏时的旧数据。你总不能每次都叫用户手动点一下刷新按钮吧?所以需要一个后台定时任务。
我用的库是apscheduler,在 Flask 启动时挂一个异步调度器,每隔 30 分钟把所有收藏数大于 0 的城市过一遍,拉取最新天气:
from apscheduler.schedulers.background import BackgroundScheduler def refresh_favorite_cities(): db_session = get_db() cities = get_favorite_city_list(db_session) # 查询收藏表,取去重城市集合 service = WeatherService() for city in cities: try: service.get_weather(city, force_refresh=True) # 强制更新,不走缓存 except Exception as e: print(f"刷新失败: {city}, error: {e}") scheduler = BackgroundScheduler() scheduler.add_job(refresh_favorite_cities, "interval", hours=0, minutes=30) scheduler.start()另外,定时任务最好用force_refresh参数绕过缓存,保证收藏列表始终展示的是“最近半小时内的天气”,否则这个定时任务就白干了。
3.4 接口清单:前后端怎么对齐
我习惯先列接口清单再写代码,这样前后端可以并行开发。这个系统的核心接口如下:
| 方法 | 路径 | 功能 | 是否鉴权 |
|---|---|---|---|
| POST | /api/register | 用户注册 | 否 |
| POST | /api/login | 用户登录,返回 Token | 否 |
| GET | /api/weather/{city} | 获取城市实时天气 | 是 |
| GET | /api/favorites | 获取收藏列表 | 是 |
| POST | /api/favorites | 添加收藏 | 是 |
| DELETE | /api/favorites/{city} | 删除收藏 | 是 |
| GET | /api/admin/stats | 管理员统计看板 | 管理员 |
| PUT | /api/admin/config | 更新 API 配置 | 管理员 |
Token 鉴权我用的是PyJWT,签发的时候把user_id和role塞进 payload,前端每个请求在 Header 里带Authorization: Bearer <token>即可。这个方案在前后端分离项目里是标配,比 Session 更契合 Vue 这种纯前端消费 API 的场景。
4. 前端开发:Vue 组件结构、页面路由和状态管理
Vue 端我用了 Vue 3 + Vite + Vue Router + Pinia。Vite 启动快、热更新爽,开发体验比 Webpack 时代舒服太多了。如果你还在用 Vue 2,也不用慌,组件思路是通用的,迁移成本很小。
4.1 页面骨架与路由
整个前端我规划了五个页面:
- 登录页
/login:用户名密码输入,调登录接口,拿到 Token 存 localStorage。 - 注册页
/register:新用户注册。 - 天气主页
/home:核心查询页,输入城市、展示当前天气和未来趋势。 - 收藏页
/favorites:自己关注的城市列表,点卡片跳转 /home 并自动查询该城市。 - 管理页
/admin:数据看板,展示热门城市、查询次数、定时任务状态。
路由配置用 Vue Router 的懒加载模式,每个页面独立打包,不让首屏一次加载全部资源:
import { createRouter, createWebHistory } from "vue-router"; const routes = [ { path: "/", redirect: "/home" }, { path: "/login", component: () => import("@/views/Login.vue") }, { path: "/register", component: () => import("@/views/Register.vue") }, { path: "/home", component: () => import("@/views/Home.vue") }, { path: "/favorites", component: () => import("@/views/Favorites.vue") }, { path: "/admin", component: () => import("@/views/Admin.vue") } ]; const router = createRouter({ history: createWebHistory(), routes });4.2 登录鉴权与路由守卫
前端鉴权别做太复杂,但也不能裸奔。核心逻辑是登录成功后把 Token 存到 localStorage,Pinia 里面放一个user状态,路由守卫统一检查:
router.beforeEach((to, from, next) => { const token = localStorage.getItem("token"); if (to.path !== "/login" && to.path !== "/register" && !token) { next("/login"); } else if (to.path === "/admin" && localStorage.getItem("role") !== "admin") { next("/home"); } else { next(); } });这里踩过一个坑:如果只是检查“有没有 token”,那任何登录用户都能进管理页。所以管理员页面必须额外校验role === "admin",后端接口也要同步鉴权,双保险,否则就是个纸糊的管理系统。
4.3 天气卡片组件的设计逻辑
主页的核心是一个天气卡片组件WeatherCard.vue,它的 Props 结构大概长这样:
props: { city: { type: String, required: true }, weather: { type: Object, required: true, // weather.temp, weather.condition, weather.humidity, weather.wind } }卡片布局我建议从上到下分四块:城市名和更新时间、大大的温度数字、天气状态图标(晴/雨/多云)、湿度风力等辅助信息。不要把所有字段一股脑全铺上去,信息层级越清晰,视觉就越专业。
天气图标处理也是个细节。我建议不要用第三方图片 URL,因为对方域名要是挂了你的图标就裂了。更稳的方案是用字体图标或 SVG 自绘,比如“晴”用一个太阳 SVG,“雨”用一朵云加几根雨线,根据天气状态码映射到对应组件。
4.4 axios 封装与接口对接的细节
axios 封装是前端最不该偷懒的地方。统一 baseURL、超时时间、请求拦截器、响应拦截器、401 跳转登录页,这些都要写好。我常用的配置如下:
import axios from "axios"; import router from "@/router"; const service = axios.create({ baseURL: "/api", timeout: 15000 }); service.interceptors.request.use((config) => { const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); service.interceptors.response.use( (response) => response.data, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem("token"); router.push("/login"); } return Promise.reject(error); } ); export default service;开发环境下如果后端接口是http://localhost:5000,你需要在 Vite 配置里设置代理,避免浏览器跨域:
// vite.config.js export default { server: { proxy: { "/api": { target: "http://localhost:5000", changeOrigin: true } } } };4.5 ECharts 折线图实现趋势分析
说到趋势分析,我推荐直接用 ECharts。Vue 3 中通过一个简单的封装组件整合图表:
<template> <div ref="chartRef" style="height: 300px;"></div> </template> <script setup> import { ref, onMounted, watch, nextTick } from "vue"; import * as echarts from "echarts"; const props = defineProps({ dates: { type: Array, required: true }, temps: { type: Array, required: true } }); const chartRef = ref(null); let chartInstance = null; const renderChart = () => { if (!chartInstance) { chartInstance = echarts.init(chartRef.value); } chartInstance.setOption({ xAxis: { type: "category", data: props.dates }, yAxis: { type: "value", name: "温度(°C)" }, series: [ { name: "最高温", type: "line", data: props.temps, smooth: true, areaStyle: { opacity: 0.15 } } ], tooltip: { trigger: "axis" } }); }; onMounted(async () => { await nextTick(); renderChart(); }); watch(() => props.temps, renderChart); </script>趋势数据怎么来?未来一周的预报数据可以从天气 API 的daily字段里取,历史数据可以从自己数据库里查近一周的weather_json解析出来。如果你想省事,直接用未来一周的预报温度画折线图也可以,但“过去一周真实记录 + 未来一周预报”两条线对比展示的效果会更高级。
5. 联调与部署:从“本机能跑”到“别人也能访问”
开发完不等于结束,联调和部署才真正检验你的项目是否完整。我见过很多人在本地跑得飞快,一到部署就各种歇菜。
5.1 本机联调最容易遇到的三个坑
第一个坑是跨域。虽然 Vite 代理能解决开发环境的问题,但如果你后端接口同时被小程序、手机浏览器等外部客户端调用,那 Flask 端也得配好 CORS。用flask-cors扩展,一行代码解决:
from flask_cors import CORS CORS(app)如果前端是 Vite 代理则在开发时可以不加,但部署后我建议两侧都配置,做到真正无跨域限制。
第二个坑是日期时区问题。国内天气 API 返回的时间有的带时区标识,有的不带。你把时间存到 SQLite 时如果不统一转成东八区时间,查询的时候会惊现“最近记录在 8 小时前”的诡异现象。建议所有后端时间统一用Asia/Shanghai时区,前端展示时再用dayjs格式化一遍。
第三个坑是中文城市名编码。用requests请求第三方接口时,一定要记得把城市名做 URL 编码:
from urllib.parse import quote url = f"https://api.xxx.com/weather?city={quote(city_name)}"不然“北京”两个字可能直接被编码成乱码,导致接口返回空数据。这个问题非常隐蔽,第一次遇到时排查了很久。
5.2 后端部署:Flask + gunicorn + 日常运维
部署我推荐用最简单的三件套:服务器用 Linux、Python 进程用 gunicorn、反向代理用 Nginx。不要一上来就搞 Docker、Kubernetes,个人项目没必要背这么重的运维负担。
gunicorn 启动命令如下:
gunicorn -w 4 -b 0.0.0.0:5000 app:app-w 4表示启动 4 个 worker 进程,并发处理能力基本够用。如果接口有耗时较长的第三方请求,建议后端接口写成异步方式,或者干脆加一个threaded=True,避免阻塞其他请求。
Nginx 配置核心就两块:把/api请求转发到后端 5000 端口,把前端构建后的dist目录作为静态文件服务:
server { listen 80; server_name your_domain.com; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/weather-web/dist; try_files $uri $uri/ /index.html; # Vue Router history 模式需要 } }try_files那一行是关键。如果忘了写,刷新/favorites页面时就会出现 404 错误,因为 Nginx 找不到这个真实路径对应的文件。这也是 Vue Router history 模式部署最常见的坑之一。
5.3 前后端版本管理与发布流程
版本管理我用 Git 分两个仓库:前端一个、后端一个,互不干扰。每次发布流程是:
- 前端
npm run build,生成dist目录。 - 把
dist上传到服务器,替换 Nginx 根目录内容。 - 后端用 Git pull 最新代码,重启 gunicorn。
这套流程简单粗暴,但是足够支撑课程设计或个人项目。等以后项目复杂度上来了,再考虑引入 CI/CD 流水线也不迟。
6. 功能验收与性能优化:如何让别人五秒内看出你的项目不错
项目做完之后,你还需要当一回“验收官”,以第三方的视角审视系统到底经不经得住演示。
6.1 常见性能瓶颈及解决方案
第一,第三方 API 请求过慢。如果天气接口偶尔要 3~5 秒才返回,前端就会一直转圈。解决思路是给每个请求设置前端超时,超时就用缓存数据兜底;缓存也没有,就返回友好提示,而不是一直白屏。
第二,数据库查询慢。SQLite 在数据量几千条时完全够用,但查询天气表时一定要给city_name加索引,否则随着数据积累,数据库全表扫描会拖慢接口响应:
class WeatherRecord(db.Model): __tablename__ = "weather_records" id = db.Column(db.Integer, primary_key=True) city_name = db.Column(db.String(50), index=True) weather_json = db.Column(db.Text) fetch_time = db.Column(db.DateTime, index=True)第三,前端打包体积大。ECharts 全量引入会让首屏加载慢到让人崩溃。解决办法是按需引入图表组件,只打包你真正用到的折线图、柱状图、提示框这几个模块:
import { LineChart, TooltipComponent, GridComponent } from "echarts/components"; import { use } from "echarts/core"; use([LineChart, TooltipComponent, GridComponent]);6.2 验收清单:最少要自测这五项
我给自己项目验收时会过一遍这张清单:
- 注册/登录流程是否顺畅?密码错误是否有提示?Token 过期后是否自动跳转登录页?
- 查一个不存在的城市,后端是否有异常捕获,前端是否给出“城市不存在”的提示而不是 500 报错。
- 收藏城市后刷新页面,收藏列表是否还在?定时任务是否真的把收藏城市的天气更新了?
- 管理员登录后能否看到统计看板?普通用户访问管理页是否被拦截?
- 断网情况下打开系统,页面是否优雅降级?至少不能白屏崩溃。
这五项如果能顺利通过,系统的健壮性已经超过大部分交作业级别的项目了。
6.3 两个让项目“显得更完整”的小技巧
我做课程设计的时候发现,两个小细节对项目印象分提升特别大。
第一个是加载态和错误态。调用接口时要有 loading 动画,接口失败时要有重试按钮或提示语。哪怕只是简单的v-if控制显示隐藏,用户体验也是天壤之别。
第二个是“更新时间戳”。在天气卡片角落放一行小字“数据更新时间:2025-06-20 10:30”,瞬间让人感觉到这是一个有真实数据流动的系统,而不是静态页面。这个字段后端接口直接返回fetch_time,前端dayjs格式化一下就搞定,成本几乎为零。
7. 难点复盘:定时任务失效、Token 过期和前端缓存问题
任何项目做完都要复盘,把踩过的坑记录下来。这个项目我前前后后做了两版,第二版里填了几个印象极深的坑,写出来给大家避雷。
7.1 定时任务“悄悄消失”的坑
第一次做定时任务时,我把BackgroundScheduler直接放在app.run()之前启动。开发环境下一切正常,但部署到 gunicorn 后发现定时任务随机失灵,有时候跑一次就不跑了,有时候压根没启动。
查了很久才发现:gunicorn 默认 spawn 4 个 worker,每个 worker 都会执行一遍启动代码,BackgroundScheduler也被实例化 4 次。于是 4 个调度器互相抢占任务,同一个城市被刷新了 4 次,把第三方 API 限额瞬间打爆。
解决方案有两个:要么把调度器独立成单独的 Python 进程运行,和 gunicorn 分开部署;要么只允许一个 worker 启动调度器,通过环境变量或文件锁做单例控制。我选用的是协议--preload配合进程判断的方式,简单说就是只让主 worker 注册调度任务。如果你的项目不急,我建议直接单独开一个进程跑调度器,最干净。
7.2 Token 过期后前端“假死”的问题
Token 默认设置了 24 小时过期,但前端不会自动刷新。于是用户挂了一个晚上,第二天打开页面,所有请求都返回 401。问题在于实际调用中,如果某个请求返回 401,前端响应拦截器已经跳转登录页了,但此时页面可能还在等待其他并发请求,结果出现跨路由跳转的竞态冲突。
解决办法是 401 时做重定向去重:
let isRedirecting = false; service.interceptors.response.use( (response) => response.data, (error) => { if (error.response && error.response.status === 401) { if (isRedirecting) return Promise.reject(error); isRedirecting = true; localStorage.removeItem("token"); router.push("/login"); setTimeout(() => { isRedirecting = false; }, 2000); } return Promise.reject(error); } );这不算完美方案,但对于个人项目来说足够稳妥,不会再出现一堆“401 重定向”连续跳转的怪现象。
7.3 前端缓存旧数据导致“天气一直不变”
刷新页面后,浏览器默认不会主动清空 localStorage 里的数据。如果前端把天气数据也存在本地,页面打开会优先取本地数据,反而显示不出后端更新的数据。我建议前端只存 token 和用户基础信息,天气数据一律走后端接口,最多在 Pinia 里做短时缓存,并绑定刷新时间。否则你在后台把缓存清掉了,用户看到的还是旧天气,排查起来非常迷惑。
8. 扩展方向:从毕设项目到可落地产品的进化路径
项目做完之后,如果你还想继续投入,下面这几个方向值得一试,每一个都能显著提升项目的价值感。
8.1 接入更多维度的气象数据
现在系统只做了温度、湿度、风力和天气状态。往上扩展可以做空气质量指数(AQI)、气压、能见度、紫外线强度、体感温度。展示形式也能更丰富,比如用环形进度条展示空气质量,用色块分级展示紫外线强度,整个页面信息密度和专业感立刻就不一样了。
8.2 增加地理定位与区域天气地图
前端可以通过浏览器navigator.geolocation获取用户经纬度,再调用后端接口逆地理编码成城市名,实现“自动定位当前城市天气”。更进一步,你想要“全国天气地图”的效果,可以结合 ECharts 的 china 地图组件,把各省会城市的天气以标记点形式标注在地图上面。这个效果在答辩或者项目展示时非常亮眼,但实现起来也就是后端多一个城市列表接口的事儿。
8.3 邮件或消息订阅推送
做一个小工具模块,让用户设置每天早晨 8 点接收“今日天气推送”,后端用apscheduler定时查库,匹配已订阅用户,调用邮件接口发送早安天气邮件。这个功能虽然简单,但涉及异步任务、模板渲染、第三方邮件服务对接,实操价值很高。而且面试聊到这个模块,对方会觉得你是真的在认真设计产品,而不只是堆 CRUD。
8.4 数据采集与可视化分析
数据库里已经攒了一堆历次查询记录,这些数据可以做更高级的分析。比如按小时统计用户查天气的高峰时段,按城市统计检索热度趋势,甚至可以做用户画像,发现哪些城市在最近一周查询次数异常上涨。配合 ECharts 的热力图、柱状图,秒变一个“天气数据分析平台”。唯一的代价就是要多设计几张统计表,但逻辑上完全是在现有数据上做聚合,不需要额外采集。
9. 项目文件结构与展示建议
最后附上我建议的完整项目目录结构,方便你搭建时心里有数:
weather-system/ ├── backend/ │ ├── app.py # Flask 入口 │ ├── config.py # 配置文件:密钥、API Key、时区 │ ├── models.py # SQLAlchemy 数据模型 │ ├── services/ │ │ ├── weather_service.py # 天气查询服务 + 三级缓存 │ │ └── stats_service.py # 统计看板服务 │ ├── api/ │ │ ├── auth_api.py # 注册/登录接口 │ │ ├── weather_api.py # 天气接口 │ │ ├── favorite_api.py # 收藏接口 │ │ └── admin_api.py # 管理员接口 │ ├── scheduler.py # 定时任务 │ └── requirements.txt ├── frontend/ │ ├── src/ │ │ ├── views/ # 页面级组件 │ │ ├── components/ # 天气卡片、图表等通用组件 │ │ ├── store/ # Pinia 状态 │ │ ├── utils/ # axios 封装、日期工具 │ │ └── router/ # 路由配置 │ └── package.json └── README.md # 项目说明文档README 一定要写得像回事,至少包含项目简介、技术栈、功能列表、启动步骤、接口文档示例。很多人忽略这一点,但它是别人(导师、面试官)看项目的第一个入口,花半小时写清楚性价比极高。
关于项目展示,我的经验是:演示前先清库清缓存,确保系统处于干净状态;演示时先走注册登录流程,再查一个城市,再收藏一个城市,再展示管理页的数据看板,一气呵成。千万不要把数据库里堆满测试数据再上台,那样会显得一点都不专业。如果条件允许,准备两张对比截图——一张是用户页面展示实时天气,一张是管理员页面展示统计图表——即使现场网络崩了也有兜底素材。
做这种管理系统类项目,技术点其实都是常规操作,拉开差距的地方在于系统设计是否完整、边界情况是否考虑周全、细节体验是否到位。从技术选型到三级缓存,从定时任务到前后端联调,每一步都踏实走完,你做出来的就不仅仅是一个“能跑的 Demo”,而是一个能拿得出去说“这是我独立设计并实现的完整系统”的作品。