
1. 为什么这单子要同时做ThinkPHP和Laravel两套前一阵刚交付完《2048-小程序》的源码紧接着又来了一个更典型的活儿微信小程序天气预报系统。需求本身不复杂但对方明确提了一句——后端要同时支持ThinkPHP和Laravel。你别说这类要求在真实接单场景里一点都不少见。很多人不理解为什么一个天气预报项目要同时支持两个框架。我实际接触下来的原因通常是这几种甲方自己手上就有一批老项目跑在ThinkPHP上希望天气系统能直接拆成模块嵌进旧后台但又想保留Laravel版本作为长期迭代主线。交付方也就是我们为了扩大成交率把同一个系统做成双框架版本后面不管甲方用哪套技术栈接手都能跑。学习型需求有人想通过同一个小程序项目反推两种主流PHP框架的写法和差异拿来做内部培训或毕业设计。无论哪种原因双框架支持意味着后端API部分要写得非常“薄”——核心逻辑不能跟框架绑死换框架只换外壳业务层和数据处理层尽量保持通用。这个思路恰恰是天气类项目最适合练手的点。那这套系统拆开来看功能模块大概是这样模块职责框架耦合度用户登录与授权小程序端code换取openid、手机号解密中城市定位与搜索经纬度逆地理编码、热门城市、城市模糊搜索低天气数据拉取与管理对接第三方天气API、原始数据入库/缓存、响应字段裁剪高需要设计跨框架的service展示与交互小程序前端页面、天气图标、未来几天预报列表无订阅消息/定时任务早晚天气推送、定时刷新缓存中项目标题里特意写了“Thinkphp和Laravel框架都支持”其实真正值钱的地方不在页面而在于这一层跨框架的服务层设计。下面我会把每个环节的实操过程都拆开讲最后附上双框架代码对比和部署时容易翻车的细节。2. 天气数据源接哪个、缓存怎么设计决定系统能不能撑住2.1 国内可用的天气API怎么选做小程序天气系统第一步不是写控制器而是确定天气数据从哪来。目前国内实际项目中用得最多的是和风天气和高德开放平台。做这套双框架版本时我推荐的做法是默认接和风天气同时在高德那边申请一个Key备用。原因有几个和风的接口粒度细像分钟级降水、生活指数、空气质量这类数据都能拿到后续想给小程序加“穿衣指数”“防晒提醒”都是现成的。高德的好处是它的Android定位SDK和Web服务API用的是同一套Key体系如果你在小程序里用腾讯地图做逆地理编码也可以直接调高德的天气接口作为降级方案。两个平台都提供免费额度个人小程序日请求量不高的情况下完全够用。和风天气接入时核心是拿到LocationId。两种拿法一是通过城市名称模糊搜索二是通过经纬度反查。小程序端用wx.getLocation()拿到经纬度后后端直接请求GET https://devapi.qweather.com/v7/weather/now?location116.41,39.92key你的Key这里有个文档里不容易注意到的点和风的经纬度传参格式是“经度,纬度”不是“纬度,经度”也不要加空格。我第一次调试时用39.92,116.41去请求返回的数据一直对不上城市排查了半天才发现是参数顺序的问题。把这个顺序写进团队文档里能少踩很多坑。2.2 两级缓存设计数据库兜底 Redis/文件提速天气数据有个特点变化不频繁但请求可能很密集——比如小程序首页一打开定位接口和天气接口会同时打过来每个用户进入一次页面就可能产生2到3次天气请求。如果没有缓存免费额度很快会被烧光。我的做法是两级缓存第一级运行缓存ThinkPHP用think\facade\CacheLaravel用Cache::store()驱动选择Redis或文件。TTL设置20到30分钟。第二级数据库表兜底存一份全量城市最近一次的天气原始数据。运行缓存失效后先读库库里也没有才去调第三方API。这样设计的直接好处是即使第三方接口短时挂了二次请求还能拿到半个小时内的旧数据页面不至于显示空白。缓存Key设计上建议用weather:now:{locationId}和weather:7d:{locationId}这样的结构。一个小技巧把当前城市名也拼进Key里日志排查时会非常直观。比如// ThinkPHP版本 $cacheKey sprintf(weather:now:%s:%s, $locationId, $cityName); // Laravel版本 $cacheKey sprintf(weather:7d:%s:%s, $locationId, $cityName);2.3 定时刷新和队列早晚推送的前置条件天气预报系统如果只是一进去看天气体验不够完整。多数带交互的版本都会加“每日早晚推送”或者“降温提醒”这就需要服务端有定时任务。ThinkPHP这边用php think timer或者配合Crontab定时访问一个内部刷新接口Laravel则用php artisan schedule:run挂在系统的crontab里。跑的任务很简单遍历常用城市列表把未来几天的预报数据主动拉一遍并刷新缓存。有一点要特别提醒小程序端的订阅消息是“一次订阅一次推送”不能把用户当成无限次打扰对象推送频率控制在每天一次或只在预警天气时推送产品口碑会好特别多。3. 小程序端页面拆解定位逻辑与天气意象的渲染3.1 首页信息架构怎么排小程序功能不复杂页面信息架构却要讲究。这套系统的小程序端我最终定了四个区块顶部当前城市、实时温度、天气现象文字。中部未来24小时或未来7天预报用横向滚动的方式排时间点。下方生活指数卡片紫外线、洗车、穿衣、运动。全局下拉刷新、定位失败时的手动城市切换入口。顶部导航栏高度这个问题很多人做小程序时会遇到。因为不同机型状态栏高度不同尤其是全面屏和刘海屏差异特别大。天气首页背景是渐变色的话顶部背景要无缝接上去就必须动态计算导航栏高度。// 小程序端获取顶部导航栏高度 const systemInfo wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuButton.top - systemInfo.statusBarHeight) * 2 menuButton.height;这段代码的效果是在任何机型上都让“城市名温度”处在安全区域内。你如果用固定像素iPhone 12和iPhone SE的显示效果会差出一大截。3.2 定位与授权getLocation的那些反向体验小程序获取天气的第一步是定位。常规写法是wx.getLocation()但它有一个使用门槛需要在小程序管理后台申请这个接口权限而且个人主体小程序对这些接口限制很多。再说一个容易被忽略的安全问题如果直接用经纬度去调天气API第三方平台返回的数据是跟着坐标走的但你的合法域名必须是HTTPS且完成IC备案否则小程序正式环境下请求会直接失败。所以调天气接口的正确链路应该是小程序 - 你的后端服务器(ThinkPHP/Laravel) - 天气API不要在小程序端直接请求和风或高德的接口。一是跨域问题二是Key会暴露在小程序包里面被扒出来后会被别人拿去刷额度。定位失败时也要有降级方案默认定位到北京同时弹出一个城市选择器让用户手动选。不要一失败就报错用户感知会很差。3.3 天气图标网上找一套不如自己规范一套天气现象图标这块设计师可能觉得直接下载一套天气素材就行但我实际做下来发现最好把图标文件名跟后端返回的天气代码严格对应起来。和风天气的天气代码是数字如100表示晴101表示多云一套完整代码表至少有三十几个状态。一定要做一个前端映射表const weatherIconMap { 100: sunny, 101: cloudy, 103: overcast, 104: overcast, 300: shower, 301: shower, 305: rain, 306: rain, 307: rain, 309: rain, 400: snow, 407: snow, 500: fog, 999: special, 9999: unknown };这样将来换后端数据源只需要改映射关系不用重新设计页面视觉。天气列表项建议用左图右文上下结构图标居中下面跟白天夜间温度范围而不是只放一个当前温度否则看不出温差整体信息量会很单薄。4. ThinkPHP版和Laravel版的同功能代码长什么样双框架支持的核心是“同一个业务逻辑两套框架外壳”。下面拿几个高频场景做对比你能直观看出两套写法各自舒服在哪。4.1 路由定义差异ThinkPHP的route/app.php或route/route.php里这样加Route::get(api/weather/now, WeatherControllernow); Route::get(api/weather/city/:city, WeatherController/searchCity);Laravel则写进routes/api.phpRoute::get(/weather/now, [WeatherController::class, now]); Route::get(/weather/city/{city}, [WeatherController::class, searchCity]);注意一个小细节ThinkPHP的路由变量用:Laravel用{}。如果你把TP的路由迁移到Laravel里直接用第一反应会报“route parameter not found”其实不是代码错是框架规则不同。4.2 控制器接收参数的业务逻辑对比ThinkPHP控制器注入请求对象非常直接namespace app\api\controller; use think\Request; class WeatherController { public function now(Request $request) { $lng $request-param(lng, ); $lat $request-param(lat, ); $city $request-param(city, ); // 调用统一服务层 $service new WeatherService(); $result $service-getNowWeather($lng, $lat, $city); return json($result); } }Laravel的视图更强调依赖注入和资源响应namespace App\Http\Controllers\Api; use Illuminate\Http\Request; use App\Services\WeatherService; use Illuminate\Http\JsonResponse; class WeatherController extends Controller { public function now(Request $request): JsonResponse { $lng $request-input(lng, ); $lat $request-input(lat, ); $city $request-input(city, ); $service new WeatherService(); $result $service-getNowWeather($lng, $lat, $city); return response()-json($result); } }本质上两者做的事情完全一样。所以写双框架版本时的最大成本不在控制器而在服务层是否独立。4.3 服务层用框架无关的方式写我的建议是服务层不要直接用Cache::这样的门面而是定义一套统一的接口方法然后再用框架各自封装。说得直白一点服务层内部只负责调外部接口和裁剪数据缓存由调用方或一个可替换的类来承载。比如天气服务的基础结构class WeatherService { private $dataSource; private $cache; public function __construct($dataSource null, $cache null) { $this-dataSource $dataSource ?: new WeatherApiClient(); $this-cache $cache; } public function getNowWeather($lng, $lat, $city ) { $locationId $this-getLocationId($lng, $lat, $city); $cacheKey weather:now: . $locationId; // 用缓存类去读而不是直接依赖框架 if ($this-cache $raw $this-cache-get($cacheKey)) { return $raw; } $raw $this-dataSource-fetchNow($locationId); // 字段裁剪、格式化统一在这里处理 $data $this-formatNow($raw); if ($this-cache) { $this-cache-set($cacheKey, $data, 1800); } return $data; } }TP版本在app\api\service\WeatherService.php里Laravel版本在app/Services/WeatherService.php里类名空间不同但核心方法一模一样。后续换数据源或加缓存策略两边只需要小改。如果你只是机械地复制粘贴业务代码到另一套框架双框架的交付成本会成倍增加。核心服务类的设计才是决定项目能不能长期维护的关键。5. 部署上线阶段最容易踩的坑域名、HTTPS和配置隔离5.1 小程序合法域名本地调试与正式环境不一致很多新手第一次上线微信小程序时卡得最久的不是功能而是“request 合法域名校验”。微信要求所有网络请求必须指向HTTPS域名且域名要从小程序后台配置。这意味着本地联调时用http://localhost或IP能通过但预览版就请求失败了。正式小程序里不能直接用IP、不能带端口。接口域名必须在mp.weixin.qq.com后台的“开发管理-服务器域名”里添加。天气项目有一个比普通项目更麻烦的点如果小程序直接请求了一个未备案域名微信接口会报10002错误。很多号称“无法定位”的bug根因其实是域名配置不对。我个人的排查顺序是先在开发者工具里“不校验合法域名”跑通业务再用手机预览真机调试最后检查证书链是否完整。证书这一块建议不要贪便宜用自签名证书在真实微信小程序环境里经常会因为证书链不完整导致请求被拦截。5.2 双框架的配置隔离.env与.env.tp同时维护TP和Laravel两套工程时最容易出事故的是配置混乱。Laravel天生有.env文件ThinkPHP也是从5.0开始支持.env两者虽然格式差不多但有几个字段名称不能混用配置项ThinkPHP变量名Laravel变量名数据库DB_HOST/DB_NAMEDB_HOST/DB_DATABASE缓存驱动CACHE_DRIVERCACHE_STORE时区DEFAULT_TIMEZONEAPP_TIMEZONE如果你的服务器上两套工程共用一个环境文件会出现“在TP里改了数据库但Laravel里读到了同一个配置”的问题。我的建议两套工程各自维护.env并在.gitignore里互相排除对方的.env同时在代码注释里强调“配置文件互不共用”。5.3 天气Key的安全与限流免费天气Key的限额很脆弱。我有一次在真实环境调试时因为忘记关掉一个自动刷新循环导致每分钟请求次数直接触顶当天下午天气接口全部返回429 Too Many Requests。针对这一点上线前必须在服务层加两个保险请求频率控制同一个locationId五分钟内最多请求一次外部API超出直接返回缓存。日请求熔断当日请求量达到额度的80%时打开“只读缓存模式”不再触发外部调用。有了这两层保险免费额度基本不会被打爆。接口不可预报时顶多用户看到的是20多分钟前的旧天气数据不会变成页面打不开体验上没有大问题。6. 接手这套源码的后续改造建议6.1 拿到项目后先做这几件事如果你是接盘者建议按下面的顺序做初始化检查别一上来就急着改页面检查两份.env的数据库、Redis、天气Key是否配置正确。用Postman或Reqable分别请求TP和Laravel的天气接口确认返回结构一致。再看小程序端的config.js里的baseUrl确认指向你部署的域名。跑一个城市搜索用例搜“北京”和搜“bj”都要有结果大小写和拼音场景都要通。在微信开发者工具中切到“真机调试”重点验证定位授权弹窗。这套系统因为是双框架接手时很容易犯一个错误只修了TP端的代码忘了同步Laravel端同样的问题。我建议把两个仓库合到同一个项目目录下用tp/和laravel/两个文件夹区分这样跑对比脚本时一目了然。6.2 从天气预报系统往外延伸的方向天气系统本身是个很小的业务但作为项目底盘它能延伸的东西不少。我在这一版基础上顺手加过两块功能都很受用根据天气代码自动生成“出行建议文案”比如有雨就提示“记得带伞”温差大就提示“推荐洋葱式穿衣”。这个能力不建议写死在逻辑里而是放到一个建议配置表里运营人员可以直接改后台。加了一个“近7日天气对比看板”把同一城市的历史平均温度跟当前预报放到同一条折线图上一眼能看出今年是偏暖还是偏冷。这个视觉效果在小程序里非常吃香用户留存率高很多。顺着这个思路后面再套一个“基于天气的推荐引擎”都很顺手空气质量差时推荐室内活动滑雪指数高时推荐户外雪场等。天气预报的接口数据本身是标准化的业务想象力才是拉开差距的地方。6.3 双框架维护的日常节奏最后说说两套框架并行维护的体会。有人在评论区或群里问同时维护TP和Laravel不会冲突吗我的答案是只要服务层足够独立两者完全可以和平共处。真正费时间的不是写代码而是改外部接口和调缓存策略。天气API响应字段偶尔会调整比如和风早期版本返回cond.txt后来调整为text这种字段变更两边都要同步修改没有捷径。所以我养成了一个习惯在服务层外面罩一层DTO也就是数据转换对象。DP层只暴露语义化字段比如$data[condition]、$data[temp]不管第三方接口内部怎么改最终给小程序端的字段都保持一致。这样外部API怎么变我只需要改一处适配器TP和Laravel两端的解析逻辑都不用动。针对这个项目我的建议是不要想着“写一次到处跑”而是老老实实跑两套但维护一套核心业务抽象。这份双框架天气预报系统做完之后你会对PHP生态的两种主流风格有更深的理解——ThinkPHP的上手速度快Laravel的路由和容器设计更规范二者其实没有绝对的优劣在天气这种轻业务下谁都能把事情做漂亮。关键是你的服务层写得干不干净那才是真正影响半年后维护体验的地方。