最近在处理一套很有意思的源码项目:ThinkPHP和Laravel框架都支持 微信小程序天气预报系统。名字带后缀_kucjz,明显是源码站交付包,但代码质量比预期高——后端同时兼容两个主流PHP框架,小程序端用原生开发,定位、城市天气、未来7天预报、生活指数这些模块全部齐了。这套项目适合三类人:做PHP后台但想快速出一套小程序REST API的;做前端想理解小程序和后端如何做登录、定位、缓存的;以及要交付“双后端兼容”需求的外包或毕设开发者。这篇文章我就把它彻底拆开讲:双框架兼容架构怎么设计、小程序端哪些模块最坑、天气数据源怎么接、部署联调时最常见的几个坑。每个环节我都会给出可直接抄走的方案和代码片段。
1. 双框架兼容架构:同一套业务逻辑如何在两个后台上跑
“ThinkPHP和Laravel框架都支持”听起来像宣传语,但真正落地的时候你会发现,这两套框架的目录结构、路由分发、自动加载机制完全不同,想做到一套业务逻辑同时兼容,不是把文件复制一份那么简单。这套项目的做法值得拆开好好看看。
1.1 目录差异与统一业务层设计
ThinkPHP 5/6 的典型目录结构是application或app下的controller、model,路由走入口文件加模块/控制器/操作三段式。Laravel 则是app/Http/Controllers,配合路由门面和中间件。两种框架的自动加载也完全不同,TP 有自己的类库导入机制,Laravel 走 Composer PSR-4。
这套项目的解决办法很干脆:业务逻辑不写死在任意一套框架的 Controller 里,而是抽到独立的common或者app\Support目录下,通过 Composer 的autoload配置给两个框架共用。两个框架里各写一个极薄的 Controller 壳子,只负责收参数、格式化返回,真正干活的是底层的业务服务类。这样做的好处有两个:
- 天气查询、缓存读写、登录态生成这些核心逻辑只维护一份,不会出现 TP 版修了 bug 但 Laravel 版忘改的灾难现场。
- 换框架时只需要重写 Controller 层和少量启动文件,业务层可以原封不动搬走。
我拿到这种双框架项目时,第一步不是急着看 Controller,而是先看composer.json里autoload指向哪个目录,通常核心业务类都在那里。这个判断方法放到其他类似源码上也管用。
1.2 用门面与服务提供者解决框架解耦
Laravel 的依赖注入和门面体系很强大,但 TP 没有对应的容器概念。这套项目避免在两个框架里各写一套依赖注入逻辑,而是在业务层做了一个简单的静态工厂:
<?php namespace App\Support; class WeatherService { private static $instance = null; public static function getInstance() { if (self::$instance === null) { self::$instance = new self(); } return self::$instance; } public function getWeather($cityId) { // 核心业务逻辑 } }这个写法不算高大上,但非常实用。Laravel 里可以app(WeatherService::class),也可以直接用WeatherService::getInstance();TP 里直接静态调用,不需要任何容器适配。单例保证了请求周期内缓存连接、天气连接只初始化一次,对性能也有帮助。
注意一点:业务层尽量别直接调用框架函数,比如 TP 的db()和 Laravel 的DBFacade。一旦用了,同一套代码在两个框架下的行为就会分叉。这套项目里的做法是把配置读取也包了一层,统一走Config::get('weather.key'),在两个框架的入口文件分别做一次桥接。如果你要自己改造一个双框架项目,这个桥接层是最值得先写出来的部分。
1.3 数据库层只依赖查询构造器
天气类小程序对数据库的依赖通常不重:一张用户表存 openid、手机号,一张城市收藏表,几张配置表就完了。这套项目里数据库操作没有用 TP 的模型也没有用 Laravel 的 Eloquent,而是直接用底层 PDO 封装了一个DataService,所有 SQL 都是原生语句。
为什么这么干?因为两套框架的模型层差异太大,Eloquent 的关联模型和 TP 的模型查询写法完全不是一回事,强行兼容只会让代码越来越绕。而 PDO 是 PHP 内置的,任何框架都能用。代价是你要手写参数绑定、封装简单的增删改查,但对于表结构简单的小程序项目来说,这点工作量完全可控。
<?php namespace App\Support; use PDO; class DataService { private $pdo = null; public function __construct($config) { $this->pdo = new PDO( $config['dsn'], $config['username'], $config['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } public function getUserByOpenid($openid) { $stmt = $this->pdo->prepare('SELECT * FROM user WHERE openid = ? LIMIT 1'); $stmt->execute([$openid]); return $stmt->fetch(PDO::FETCH_ASSOC); } }数据库连接配置照样走Config::get()桥接,这样无论你切到 TP 还是 Laravel,都只需要改一份环境变量配置。
2. 小程序端核心模块拆解与实现细节
小程序端用的是原生框架,没有引入 uni-app 或 Taro 第三层。对于天气这种轻交互项目,原生开发的好处是包体小、启动快,也不用被框架的编译链拖着走。但这不代表没坑,登录获取手机号、自定义顶部导航栏、页面生命周期刷新,这三个点是整个前端最容易出问题的地方。
2.1 登录与会话设计:getPhoneNumber 换取手机号
天气类小程序通常会在用户第一次进来时引导登录,目的是保存城市收藏和推送订阅。微信推荐的做法是wx.login拿到 code,后端拿 code 去换 openid 和 session_key,再自己签发一个登录态 token 给小程序端存起来。
代码走一遍是这样:
wx.login({ success: async (res) => { const { code } = res; const loginRes = await request.post('/auth/login', { code }); if (loginRes.data.code === 0) { wx.setStorageSync('token', loginRes.data.data.token); } } });后端拿到 code 后调用微信的code2Session接口,换取 openid 和 session_key。注意 session_key 必须存起来,后续如果要用老方式解密用户手机号或敏感信息,它不能丢。然后用 openid 作为业务主键查用户表,老用户直接签发 token,新用户先创建一条记录再签发。
再看手机号快捷获取。这是当前小程序拿手机号的主流方式:
<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> 一键登录 </button>onGetPhoneNumber(e) { if (e.detail.code) { request.post('/auth/phone', { code: e.detail.code }).then(res => { // 手机号已绑定到当前账号 }); } }后端拿e.detail.code调用phonenumber.getPhoneNumber接口,拿到纯号码和区号,再绑定到上一步的 openid 上。这里有个很容易踩的坑:该接口要求小程序是企业主体且已完成认证,个人主体小程序调用会直接报权限错误。我之前帮朋友做天气小程序时就卡在这,最后只能降级成“用户名+头像”的简化登录方式,等主体符合要求再开放手机号功能。
2.2 自定义顶部导航栏:状态栏与胶囊按钮的高度计算
天气页面的顶部通常要放城市名、定位图标、温度显示和一个刷新按钮,原生导航栏只支持胶囊按钮加标题,放不下这么多元素。所以这套项目用了自定义导航栏,页面 JSON 里配置:
{ "navigationStyle": "custom", "navigationBarTextStyle": "white" }自定义导航栏的核心工作是精确计算顶栏高度,不然 iPhone 和 Android 的差异会把布局顶得乱七八糟。正确公式是:
const system = wx.getSystemInfoSync(); const menu = wx.getMenuButtonBoundingClientRect(); const statusBarHeight = system.statusBarHeight; const navBarHeight = (menu.top - statusBarHeight) * 2 + menu.height;用这个组合高度去撑起自定义导航栏的占位视图,再往下才是天气内容。注意 iPhone 14 系列以上有灵动岛,statusBarHeight会比老机型高,不要硬编码任何高度值,每个页面进入时都重新取一次,否则换机型就错位。
再提一个细节:自定义导航栏不支持默认的页面下拉背景回弹效果,要配合page的背景色设置,不然下拉的时候会露出白色原生底。可以把page的background-color和导航栏背景设成同一个渐变色。这套项目的天气头图背景每次刷新会按当天天气切换,这个交互就是靠自定义导航栏承载的。
2.3 天气卡片渲染与页面生命周期刷新
天气页面的数据渲染结构大概分成三块:今日实时天气(温度、天气现象、体感温度)、未来 7 天预报(横向滚动)、生活指数(紫外线、降水概率、风力等级)。
接口返回的数据结构是标准 JSON,小程序端直接在onLoad里请求接口,把返回值 setData 到页面。我建议把渲染数据在 setData 之前做一层预处理,比如把天气代码映射成图标路径、把温度四舍五入、把风向风速合并成展示文案。这样 WXML 模板保持干净,后面调整 UI 不需要动业务数据。
生命周期刷新有一个很实用的小技巧。天气数据还涉及“用户切后台再切回来”的场景:用户早上看完天气放到后台,中午重新打开,如果页面还停留在 setData 的旧数据,体验很糟糕。在onShow里做一次静默刷新,同时用时间戳做节流,距离上次请求超过 10 分钟才真正重新拉数据,否则直接读缓存。这样既不浪费请求,又能保证数据新鲜。
onShow() { const lastTime = this.data.lastRefreshTime || 0; if (Date.now() - lastTime > 10 * 60 * 1000) { this.refreshWeather(); } }另外别忘了在onHide或onUnload里清理定时器。有些天气小程序会在页面里做一个自动刷新倒计时,清理不及时会造成 setData 报错“setData after destroyed”,排查起来很折腾。
2.4 定位、城市选择和多源兜底
天气系统的定位链路是:用户打开小程序,wx.getLocation拿经纬度,后端用经纬度查天气;用户手动选择城市时,直接用城市名或者城市 ID 查天气。实测下来最稳定的方案是:经纬度优先,城市名兜底。
原因是天气服务商的“按经纬度查询”接口精度最高、实时性最好,返回的天气准确度远超按城市名搜索。但wx.getLocation有一个权限问题:用户拒绝授权后,接口会直接 fail。这套项目的兜底策略是四层降级:
- 第一优先:用户手动选择的城市,存到 storage,永远优先。
- 第二优先:上次定位成功的城市。
- 第三优先:当前定位。
- 第四优先:默认城市(比如北京)。
城市选择器里给一份内置的热门城市列表,百来个重点城市写进一个 JS 文件,选城市时不走网络请求,响应很快。这个体验细节很关键,做小程序的人都知道,城市选择列表一旦走接口,快则 300ms,慢则卡半天,用户早就划走了。
3. 后端接口设计规范与天气数据源接入
小程序端和后端的接口约定决定了整个联调效率。这套项目的接口风格比较老派但非常好用:统一 POST,统一返回 JSON,错误码清晰。下面把我在实际改造中沉淀下来的完整方案写出来。
3.1 统一返回结构与错误码约定
无论请求哪种接口,后端统一返回这个格式:
{ "code": 0, "message": "success", "data": { "city": "杭州", "temp": 28, "condition": "晴", "daily": [] } }code为 0 表示成功,非 0 表示具体错误。小程序端封装一个request方法,在返回层统一处理错误码,遇到 401 就清理 token 跳登录页,遇到天气接口异常就提示用户下拉重试。这里给一张常用错误码表:
| 错误码 | 含义 | 前端处理 |
|---|---|---|
| 0 | 成功 | 正常渲染 |
| 10001 | 参数缺失或格式错误 | 提示“请求参数不完整” |
| 10002 | 定位失败或无定位权限 | 提示使用默认城市 |
| 10003 | 天气服务商返回异常 | 展示缓存数据并提示稍后刷新 |
| 10004 | 登录态失效 | 清理 token 并重新登录 |
注意这个 10002 和小程序网络错误里常见的request:fail码是两回事,前后端约定错误码时不要混用,联调时非常容易看岔。建议前后端共用一张错误码表,加到项目文档里,避免口头约定。
3.2 天气服务商选型与关键参数映射
目前国内用得最多的天气数据服务商是和风天气和彩云天气。和风天气免费版支持按城市 ID、经纬度、IP 查询实时天气和 7 天预报,免费额度对个人项目完全够用;彩云天气的强项是分钟级降水预报,在天气类 App 里常用它做“几点下雨”提醒。这套项目主用的是和风天气,我改造时把彩云天气作为备选,做了一层简单的数据源切换。
和风天气接口的 key 申请后在后台创建项目就能拿到,请求示例:
https://devapi.qweather.com/v7/weather/now?location=120.18,30.06&key=你的KEY返回的 JSON 字段要做清洗和映射,不能直接扔给前端。我的映射表大概是这样的:
obsTime→ 数据时间temp→ 当前温度icon→ 天气代码,前端映射成对应图标text→ 天气现象文本,比如晴、多云windDir+windScale→ 风向和风力,拼成“东南风 3 级”humidity→ 相对湿度
和风天气的icon字段是字符串数字,前端要自己维护一套图标映射,不能假设后端永远返回稳定字面量。这属于天气类项目的基本素养。
后台请求外部 API 的时候必须注意超时控制。PHP 默认 curl 超时时间可能很长,用户界面早就转圈了。实测下来,天气接口的 curl 超时设置在 3 到 5 秒比较合理,超过就直接返回缓存数据,不要让用户的页面卡死。
3.3 三级缓存与主动刷新机制
天气数据天然适合缓存:同一城市同一时间段的天气数据几乎不会变,频繁请求天气服务商纯属浪费。这套项目的缓存策略很明确,分三级:
第一级是 Redis,key 设计成weather:{cityId}:{date},TTL 设置 1800 秒,半小时过期。第二级是本地文件缓存,后端跑在单机或者没有 Redis 的环境时,写到runtime/cache目录,同样按城市分文件。第三级才是直接请求天气服务商。
public function getWeather($cityId) { $cacheKey = 'weather:' . $cityId . ':' . date('YmdH'); $cached = Cache::get($cacheKey); if ($cached) return $cached; $data = $this->requestFromWeatherApi($cityId); Cache::set($cacheKey, $data, 1800); return $data; }主动刷新机制用在两个场景:一是用户下拉小程序页面时,前端强制刷新并清掉当前城市缓存;二是管理后台手动刷新全量缓存。为了防止下拉刷新瞬间大量并发打到天气服务商,可以做一个简单的请求合并:同一城市同一分钟内的刷新请求只放一个到服务商,其他请求等待同一个 Promise 返回。这个并发控制非常实用,实测能省掉约三分之二的无效上游请求。
4. 项目运行、真机联调与高频问题实录
源码能跑起来,和能在真机上跑起来,中间隔着很长一段路。特别是微信小程序,本地开发者工具和真机的环境差异会带来一堆莫名其妙的问题。这一部分是我在实际运行这类源码项目时总结出来的经验和排查清单。
4.1 拿到源码后如何快速跑起来
第一步,先把代码目录结构看明白。这套项目的源码里有tp5入口和laravel-app入口两个子目录,外加common公共目录、miniprogram小程序目录。不要急着删任何一个入口,先用其中一个跑通,再用另一个对照。
第二步,配置 Web 服务器把站点指向public目录。TP 和 Laravel 的入口文件都是public/index.php,但目录结构不同,直接套用同一个 Nginx 配置大概率打不开。我用的测试环境是把两个入口分别配置成两个站点,快速验证。
第三步,导入数据库 SQL。源码包里通常带sql文件,导入后修改 TP 的.env或者 Laravel 的.env,填好数据库连接信息。再申请一个和风天气的 key,填到配置文件里。这两处配置是所有环境问题的头号来源。
第四步,小程序端导入miniprogram目录,修改app.js里的接口域名,把 appid 换成自己的测试号。开发者工具里勾选“不校验合法域名”,本地开发直接后端 IP 也能调通。
4.2 request 合法域名与真机预览
开发者工具里关掉域名校验就能联调,但真机预览不行。微信小程序的wx.request强制要求请求域名是 HTTPS,且必须在微信公众平台后台“开发管理-服务器域名”里把域名加到 request 合法域名列表里。这一步少配置一个,真机就白屏。
真机联调另一个坑是 IP 白名单。如果后端部署在腾讯云或者阿里云,安全组没放行小程序服务器出口 IP,接口在开发者工具里正常、真机上一片红。排查方法很简单:真机预览时打开 vConsole,看具体报错是request:fail、url not in domain list还是403,每类错误对应的方向完全不同。
现在的微信小程序还多了一道隐私协议流程。wx.getLocation和getPhoneNumber都属于隐私接口,需要在后台“用户隐私保护指引”里声明使用目的,并配置对应接口。没有这个配置,真机上授权弹窗不会出现,直接走 fail 回调。这个坑在 2023 年之后的新版本基础库里尤其明显。
4.3 高频问题排查表
我把运行这套项目时实际遇到的问题整理成了一张速查表,以后遇到类似项目可以直接对号入座:
| 问题表现 | 可能原因 | 解决办法 |
|---|---|---|
真机报url not in domain list | 后端域名未配置到合法域名 | 微信公众平台后台添加 request 合法域名 |
真机报request:fail | 证书链不完整、后端 IP 白名单未放行,或请求被防火墙拦截 | 检查 HTTPS 证书、安全组和 WAF 配置 |
getPhoneNumber返回权限错误 | 小程序是个人主体,或未开通接口权限 | 更换企业认证主体,或降级简化登录 |
| 定位一直 fail | 未配置隐私指引,或用户关闭定位授权 | 后台配置隐私接口并申请权限,前端做城市兜底 |
| 天气数据空白 | 天气 key 过期,或免费额度用尽 | 登录天气服务商后台检查余额,查看后端日志里的status字段 |
| 顶部导航栏错位 | 高度按写死的数值适配了某款机型 | 每页重新计算状态栏高度和胶囊位置,不硬编码 |
| 开发者工具正常、真机接口 403 | 后端只允许本地访问,或出口 IP 白名单限制 | 检查部署环境的安全策略 |
4.4 关于这类源码项目我的几点避坑建议
双框架兼容的源码项目,最大的优势是你可以先跑通其中一个入口,通过它理解全部业务逻辑。我最常推荐的做法是先用 TP 入口打通,因为 TP 的配置结构更直白,定位问题更快;理解透彻之后,再对照 Laravel 入口的 Controller 壳子,很快就能看懂两套框架在适配层上做了什么。
天气 key 一定不能放在小程序前端代码里。小程序一旦发布,代码在用户手机上可以被反编译,key 直接暴露会被别人刷接口,额度耗尽后整个系统瘫痪。正确做法是后端统一代理请求天气 API,前端拿不到 key,后端再对请求做频率限制。这属于安全底线级别的要求,不是可选项。
给用户做天气数据展示时,建议保留一份最近一次成功请求的缓存数据。天气接口偶尔会 500 或者超时,这种时候展示“旧数据加一个更新时间提示”,远好过页面大空白。用户能接受数据稍旧,但不能接受功能失灵。
我后来在实际交付中还发现一个小点:天气类小程序的留存率,很大程度上取决于“早上打开能不能一眼看到今天该穿什么”。如果项目里只做了温度和天气现象,建议再加一句简单的穿衣建议文案,比如“温度较高,适合短袖,午后可能有阵雨建议带伞”。这个文案在后端根据天气代码和温度区间生成,前端只是展示,改动成本很低,但用户感知非常强。
这套项目还有不少可以扩展的空间:比如早晚天气定时推送、极端天气预警的订阅消息、城市收藏多端同步。如果你本身有 PHP 和小程序的基础,把它吃透之后,再做一个同类的城市服务类小程序会非常快。我个人在实际操作中的体会是:拿到这类源码项目,不要急着换框架、不要急着重构目录,先把一条完整链路跑通——从wx.login到后端返回天气数据,再到页面渲染成功,后面所有优化都建立在“全链路已通”这个前提上。把这条链路跑通,这套代码就是你的了。