资讯详情 微信公众号天气推送实战:模板消息与订阅通知全链路开发指南
📅 2026/10/21 5:28:50
简介这份资源面向具备少量编程基础、希望为伴侣定制微信消息推送的开发者围绕公众号每日自动推送天气与纪念日通知这一场景提供可线上运行的完整实现方案。包内共5个文件以py主程序、yml工作流配置、txt配置与说明、md文档为主压缩包约5KB体积轻巧便于直接部署到GitHub Actions实现定时触发。资源区分最新版、简化版、星座版、星座加天气版及php版等多种形态并附带消息模板需自行申请天行、和风API密钥后即可使用。目前已有7695人学习下载热度较高。读者可从中获得可直接运行的源码、消息模板与线上自动化配置思路理解定时任务、接口调用与消息推送的串联方式并参考不同版本的取舍快速搭建属于自己的天气与纪念日提醒服务。1. 从一条“今天降温记得加衣”说起公众号天气推送到底怎么跑通有个做后端的朋友去年冬天想给女朋友每天定时发一条天气提醒又不想用第三方 App 的模板消息就琢磨着用微信公众号自己搭一套。他原以为要写爬虫、要买服务器、要接短信网关结果折腾一晚上发现真正跑通的核心链路只有三段——定时触发、调天气接口、调微信推送接口。难的不是写代码是搞清楚公众号的几种消息类型到底哪种能主动推、哪种只能被动回。这套教程要解决的就是这件事用微信公众号的模板消息现在叫订阅通知能力配合一个免费的天气 API每天固定时间把指定城市的天气推给指定的人。适合两类人一是想练手公众号服务端开发的新手二是想给家人做个小工具但不想碰复杂架构的开发者。整条链路不依赖任何付费服务一台能跑 Python 的机器就够。2. 公众号消息推送的三种通道为什么模板消息是唯一解2.1 客服消息、模板消息、订阅通知的区别微信公众号能主动触达用户的消息通道常见的有三种但真正适合“每日定时推送”场景的只有一种。客服消息的限制最明显用户必须在 48 小时内和公众号有过交互你才能给他发消息。这意味着如果女朋友两天没点开公众号推送就断了。对于“每天提醒天气”这种场景这个限制直接判死刑。模板消息是早期最常用的方案开发者申请一个模板拿到 template_id然后按格式填充数据调用接口。它的优势是不受 48 小时限制只要用户关注了公众号就能推。但 2020 年之后微信逐步收紧新注册的公众号基本申请不到模板消息权限老账号也在被迁移。订阅通知是模板消息的替代品逻辑变成用户主动订阅一次你才能推一次。听起来麻烦但实际用起来可以引导用户一次性订阅多条比如一次订阅 30 天每天消耗一条。对于给女朋友做天气推送这种场景让她点一次订阅、管一个月完全可接受。提示如果你用的是老公众号且模板消息还能用直接用模板消息最省事。新号就走订阅通知别在模板消息上浪费时间。2.2 天气数据源的选择与接口参数天气 API 的选择标准就三条免费额度够用、返回字段包含温度和天气描述、接口稳定不需要密钥轮换。常见做法是用和风天气的免费版每天 1000 次调用对个人推送绰绰有余。调用天气接口的核心参数参数含义示例值location城市编码或经纬度101010100keyAPI 密钥你的 keylang语言zhunit温度单位m摄氏度返回的 JSON 里now.temp是实时温度now.text是天气描述daily[0].tempMax和tempMin是当天最高最低温。推送内容一般取这几个字段拼一句话就够了。2.3 最小可跑通的推送链路整条链路的代码结构其实很短。先写一个获取天气的函数import requests def get_weather(city_code, api_key): # 和风天气实时接口city_code 是城市编码 url https://devapi.qweather.com/v7/weather/now params { location: city_code, key: api_key, lang: zh, unit: m } resp requests.get(url, paramsparams, timeout10) data resp.json() # 返回温度、天气描述、体感温度 return { temp: data[now][temp], text: data[now][text], feels_like: data[now][feelsLike] }这段代码里timeout10是必须加的天气接口偶尔会慢不设超时会把整个推送流程卡死。city_code用和风天气的城市编码不是城市名需要提前查好。然后是推送函数以订阅通知为例import requests import json def send_subscribe_msg(access_token, openid, template_id, weather_data): # 订阅通知接口地址 url fhttps://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token{access_token} payload { touser: openid, template_id: template_id, page: pages/index/index, data: { thing1: {value: weather_data[text]}, number2: {value: weather_data[temp] ℃}, thing3: {value: 体感 weather_data[feels_like] ℃} } } resp requests.post(url, datajson.dumps(payload), timeout10) return resp.json()access_token需要提前获取有效期 7200 秒不能每次都重新请求要缓存。template_id是你在公众号后台创建订阅模板时拿到的。data里的字段名thing1、number2 等必须和模板定义的字段一一对应写错了会报 47003 错误。最后用定时任务串起来import schedule import time def daily_push(): weather get_weather(101010100, your_api_key) token get_access_token(your_appid, your_secret) send_subscribe_msg(token, user_openid, your_template_id, weather) # 每天早上 7 点推送 schedule.every().day.at(07:00).do(daily_push) while True: schedule.run_pending() time.sleep(60)schedule库适合单机跑如果部署在服务器上可以用 cron 替代。time.sleep(60)是轮询间隔60 秒精度对天气推送完全够用。3. 把推送做成能长期跑的服务access_token 缓存与异常兜底3.1 access_token 的缓存策略access_token 是整条链路里最容易翻车的地方。微信官方文档写得很清楚有效期 7200 秒且获取次数有限制。如果你每次推送都重新获取一天两次可能没事但如果推送频率高或者有多个任务共用很快就会触发频率限制。常见做法是用一个简单的文件缓存import json import time import os TOKEN_FILE token_cache.json def get_access_token(appid, secret): # 先读缓存如果没过期就直接用 if os.path.exists(TOKEN_FILE): with open(TOKEN_FILE, r) as f: cache json.load(f) if cache[expires_at] time.time(): return cache[access_token] # 缓存过期或不存在重新请求 url https://api.weixin.qq.com/cgi-bin/token params { grant_type: client_credential, appid: appid, secret: secret } resp requests.get(url, paramsparams, timeout10) data resp.json() # 写入缓存提前 300 秒过期避免边界情况 cache { access_token: data[access_token], expires_at: time.time() data[expires_in] - 300 } with open(TOKEN_FILE, w) as f: json.dump(cache, f) return cache[access_token]这里提前 300 秒过期是个血泪经验。微信返回的 expires_in 是 7200但实际有效时间可能略短如果卡着点用偶尔会遇到 token 刚过期的情况。提前 5 分钟刷新基本不会出问题。3.2 推送失败的排查路径推送失败时微信接口会返回 errcode 和 errmsg。几个高频错误errcode含义解决方向40001access_token 无效检查缓存逻辑重新获取40003openid 错误确认用户是否关注了公众号43101用户拒收用户关闭了订阅通知权限47003模板参数不匹配检查 data 字段名和模板定义排查顺序建议先看 errcode如果是 40001 就清掉 token 缓存重试如果是 47003 就去公众号后台核对模板字段如果是 43101说明用户端关了订阅需要引导重新订阅。3.3 用日志把每次推送记录下来长期跑的服务一定要有日志不然出问题只能靠猜。最简单的做法是每次推送后写一行记录import logging logging.basicConfig( filenamepush.log, levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s ) def daily_push(): try: weather get_weather(101010100, your_api_key) token get_access_token(your_appid, your_secret) result send_subscribe_msg(token, user_openid, your_template_id, weather) logging.info(f推送结果: {result}) except Exception as e: logging.error(f推送异常: {e})日志里记录接口返回的完整 JSON出问题时直接搜 errcode 就能定位。format里的%(asctime)s是时间戳方便按天排查。4. 避坑指南定时任务、模板字段与用户订阅的五个坑4.1 定时任务在服务器上跑偏时区现象本地测试 7 点准时推送部署到服务器后变成下午 3 点才推。原因服务器默认时区是 UTCschedule.every().day.at(07:00)按 UTC 时间执行比北京时间晚 8 小时。解决在代码开头强制设置时区或者用 cron 时指定CRON_TZAsia/Shanghai。Python 里可以加import os os.environ[TZ] Asia/Shanghai4.2 模板字段名写错导致 47003现象接口返回{errcode: 47003, errmsg: argument invalid}。原因订阅通知模板的字段名是固定的比如thing1、number2、date3不能自己编。很多人以为可以随便起名结果一直报错。解决去公众号后台的订阅通知模板详情页看清楚每个字段的 key 和类型。thing类型最多 20 个字符number类型只能是数字超长或类型不对都会报错。4.3 用户取消订阅后推送静默失败现象推送接口返回成功但用户没收到消息。原因用户可能在微信里关闭了订阅通知权限但接口不会报错只是消息不送达。解决定期检查用户的订阅状态或者在推送内容里加一句“如果收不到请重新订阅”。更稳妥的做法是每次推送后记录返回的 errcode如果连续多次返回 43101就主动提醒用户。4.4 access_token 被其他服务刷新导致失效现象推送突然报 40001但缓存文件里的 token 还没过期。原因如果你有多个服务共用同一个公众号的 access_token其中一个服务刷新了 token另一个服务缓存的旧 token 就失效了。解决要么所有服务共用一个 token 缓存文件要么用中控服务统一管理 token。个人项目一般只有一个推送服务这个问题不常见但如果你同时跑了其他公众号功能就要注意。4.5 天气接口返回空数据导致推送内容为空现象推送成功但内容显示“℃”温度是空的。原因天气接口偶尔会返回空数据或者城市编码写错了data[now][temp]取不到值。解决在获取天气后加一层校验def get_weather(city_code, api_key): # ... 请求逻辑 ... if now not in data or temp not in data[now]: raise ValueError(f天气数据异常: {data}) return {...}抛出异常后定时任务里捕获并记录日志当天跳过推送第二天再试。5. 进阶技巧把推送做成可配置的多城市多用户服务5.1 用配置文件管理城市和用户当你想给多个人推送不同城市的天气时硬编码就不行了。常见做法是用一个 JSON 配置文件{ users: [ { openid: user_openid_1, city_code: 101010100, city_name: 北京 }, { openid: user_openid_2, city_code: 101020100, city_name: 上海 } ] }然后推送逻辑改成遍历import json def push_all(): with open(config.json, r) as f: config json.load(f) token get_access_token(your_appid, your_secret) for user in config[users]: try: weather get_weather(user[city_code], your_api_key) result send_subscribe_msg(token, user[openid], your_template_id, weather) logging.info(f{user[city_name]} 推送结果: {result}) except Exception as e: logging.error(f{user[city_name]} 推送异常: {e})这样加人只需要改配置文件不用动代码。5.2 推送内容的个性化拼接模板消息的字段是固定的但你可以通过拼接让内容更自然。比如把天气描述和温度拼成一句话def build_message(weather, city_name): text weather[text] temp weather[temp] feels weather[feels_like] # 根据天气类型给不同建议 if 雨 in text: advice 记得带伞 elif int(temp) 10: advice 多穿点 elif int(temp) 30: advice 注意防暑 else: advice 天气不错 return f{city_name}今天{text}{temp}℃体感{feels}℃{advice}这段拼接逻辑放在推送之前把结果填到模板的thing字段里。注意thing类型有 20 字符限制拼接后如果超长会被截断需要提前控制。5.3 用重试机制应对偶发网络抖动天气接口和微信接口都可能偶发超时加一个简单的重试import time def retry_request(func, retries3, delay5): for i in range(retries): try: return func() except Exception as e: if i retries - 1: raise logging.warning(f第 {i1} 次失败{delay} 秒后重试: {e}) time.sleep(delay)调用时把get_weather或send_subscribe_msg包进去。retries3和delay5是经验值大部分网络抖动 5 秒后就能恢复。5.4 验证推送是否真的送达接口返回成功不代表用户收到。最直接的验证方法是让用户截图但长期跑的服务不能靠人。一个折中方案是在推送内容里加一个只有你知道的标记比如在thing字段末尾加一个特殊字符然后定期问用户有没有收到带标记的消息。更工程化的做法是接一个简单的回调在订阅通知的page参数里指向一个带参数的页面用户点开后你的服务端能收到一次访问记录。但这需要额外的前端页面对个人项目来说有点重。我自己的习惯是每次改完推送逻辑先给自己的微信号推一条测试消息确认收到后再切到正式用户。从那以后我每次调整模板字段或 token 逻辑都强制走一遍“自己先收一条”的流程省了很多来回排查的时间。希望帮到你。本文还有配套的精品资源点击获取