白黎挪车 v1.3.9 独立版:把微擎模块彻底剥离成独立 PHP,附完整源码分享

写在前面

前阵子想给自己弄一套「扫码挪车」的小程序——车停在外面,别人需要挪车的时候,扫一下贴在挡风玻璃上的码,就能在不看到我真实手机号的情况下联系到我。

找了一圈现成方案,发现大部分都是微擎模块形态:功能是完整的,但必须装在微擎框架里才能跑。微擎本身是个挺重的系统,为了一个挪车功能去搭一整套框架,怎么想都不划算;而且真机跑起来一旦哪里出问题,排查链路会被拉得很长(框架层、模块层、还有各种授权校验)。

所以这段时间我做了一件事:把整个模块从微擎里完整剥离出来,改写成独立 PHP 程序。不依赖微擎,宝塔面板建个站、上传、装完就能跑,界面也做了一些调整。

整个过程踩了不少坑,也确实值得记录一下。文末附完整源码包(后端 + 小程序端),有需要的可以拿去用。

一、成品是什么样

先把功能说清楚,这样你能判断是不是自己需要的。

车主侧(小程序)

功能说明
微信授权登录支持官方头像昵称填写能力,也可以跳过
车主绑定车牌号 + 手机号绑定,支持换绑
挪车码生成批量生成、小程序码、圆形码成品图,可下载打印
码管理查看、换绑、删除、放大保存相册
停车位置记录反向找车用,手动记录停车位置
挪车记录谁在什么时候扫了码、发了什么留言
订阅消息微信挪车通知授权,带累计授权次数记账
隐私号/短信通知两种通知通道都支持
带货微信小店精选商品(可选)

路人侧(扫码,完全不需要登录)

这是我觉得设计上最关键的一点:路人扫码进来不需要授权、不需要登录、不需要关注,直接就能看到车牌和通知按钮。真到了要挪车的场景,谁有耐心先授权登录再帮你挪车?

路人可以选三种方式联系车主:

  1. 隐私号外呼 —— 平台分配临时号码,双向都看不到真实手机号
  2. 平台发短信 —— 直接给车主手机发挪车短信
  3. 客户端降级 —— 短信通道没配或者额度用完时,弹出文案让路人用自己的手机发

第 3 种是我后来加的兜底。因为微信订阅消息的额度是不可查询的,你永远不知道车主那边还剩几次,一旦发不出去,路人就会卡在那里什么都做不了。

后台

会员管理、车码管理(按批次,进度可视化)、挪车记录、精选商品、广告位、菜单管理、帮助中心、参数设置……

车码管理这块我做了个实时进度展示——每个批次已绑定多少 / 总共多少,一眼看得出来哪批码发下去有没有被用起来。

二、技术上的几个核心决策

决策 1:为什么是「兼容层」而不是「重写」

一开始我考虑过直接按业务逻辑重写一遍。算了下工作量就放弃了——后台有 11 个管理页、小程序端 15 个页面,重写等于把整个项目做两遍,而且界面还原度很难保证(原作者的前端样式是精调过的)。

最后选的方案是:写一层兼容层,把微擎提供的东西全部重新实现一遍。

具体来说,业务代码里到处在用这些微擎提供的能力:

  • pdo_get / pdo_update / pdo_fetchall 等数据库函数
  • $_W / $_GPC 两个全局变量
  • {if}{loop}{php} 那套模板语法
  • WeModule / WeModuleSite 这些基类
  • 模块的生命周期(doPageXxx 接口自动路由)

这些全部在 core/ 目录里自研实现:

core/
├── bootstrap.php    引导:常量、$_W、$_GPC、配置加载
├── db.php           pdo_* 系列函数(PDO 预处理实现)
├── func.php         ihttp_* / message / pagination / url 等
├── template.php     模板引擎(兼容 {if}{loop}{php}{$var})
├── admin.php        WeBase / WeModule / WeModuleSite / WeModuleWxapp
├── router.php       路由分发
├── wechat.php       WeChatClient(登录、小程序码、订阅消息、小店)
└── controller/      auth / wxapp / mobile / landing

好处非常明显:addons/liuer_car/ 里的业务代码几乎一行都不用改,界面自然就是零差异。坏处是兼容层要做到足够细,有些微擎的隐式行为不看源码根本猜不到(后面踩坑章节会讲)。

决策 2:数据库表结构完全保留

表名、字段名全部沿用原样,前缀还是 liuer_car_。

这么做有两个考虑:一是业务代码里 SQL 到处都是,改表名等于全项目手术;二是如果以后有人想从微擎版迁过来,直接把数据导过去就能用。

一共 27 张表:

  • 20 张模块表:liuer_car_codes(码本体)、liuer_car_codes_log(批次)、liuer_car_users(车主)、liuer_car_movelogs(挪车记录)、liuer_car_sms、liuer_car_virtuals……
  • 7 张独立表:替代微擎框架表的 core_users(管理员)、core_sessions、core_cache、account_wxapp(小程序配置)、uni_account_modules(模块配置)等

建表语句全在 core/schema.json 里,install.php 安装时自动按它建表。

决策 3:小程序端保留 we7 公共库

小程序端的 we7/resource/js/util.js 是微擎小程序的请求封装,负责拼 URL、算签名、处理登录态、401 自动重登这一整套。

这部分逻辑自己重写风险很大——签名算法、参数顺序、state 传递方式任何一个细节错了,所有接口都会失败,而且报错信息往往很模糊。所以保留了原库,只在自己需要的地方做加固。

三、踩过的坑(这部分才是干货)

说实话,剥离过程里技术难度不高,真正耗时间的是那些不报错但行为不对的坑。挑几个有代表性的说说,如果你也在做类似的事,希望能帮你省点时间。

坑 1:$_W / $_GPC 不是超全局变量

微擎里这两个变量看起来像超全局,实际上它们只在入口文件的作用域里存在。业务代码在函数或方法体内直接用:

function some_function() {
    // ❌ 这里读到的是 null,而且不报错!
    $uid = $_W['uniacid'];
}

必须在函数内先 global $_W, $_GPC;,否则读到 null 还不报错,排查起来极其痛苦。

我因为这个踩了两次坑:一次是后台的表单提交校验全都失效(checksubmit() 读到 null 恒为 false),一次是小程序接口所有参数都变成空。

后来专门写了个静态扫描脚本,把所有函数体内用到这两个变量但没 global 的地方全揪出来。

坑 2:模板引擎的 {php} 不能含花括号

自研的模板引擎用的是非贪婪正则:

$tpl = preg_replace('/\{php\s+(.+?)\}/is', '<?php $1 ?>', $tpl);

(.+?) 是非贪婪匹配,遇到第一个 } 就停了。所以模板里写这种代码会直接崩:

{php if ($a) { echo $b; } }   ← 被截断成 {php if ($a) ,后面的全乱套

解决办法有两个:

  • 用替代语法:{php if ($a): echo $b; endif; }
  • 更好的做法:把复杂逻辑封装成 PHP 函数,模板里只写单表达式调用
  {php echo liuer_admin_font_css(); }

第二种好处是逻辑集中、模板干净,改起来也方便。这个规则后来成了我改模板的铁律。

坑 3:二进制图片别假设只有一种格式

微信生成小程序码的接口 wxa/getwxacodeunlimit,文档说是返回 PNG,实测返回的是 JPEG。

这个问题隐蔽在于:图片能正常保存、能正常打开,看起来没问题。但如果你按 PNG 去处理(比如用 GD 函数、或者判断文件头 \x89PNG),就会失败。更坑的是,接口在出错时会返回 JSON,我原来的判断逻辑是 if ($content[0] === '{') 就当错误处理——如果 JSON 前面带了 BOM 头,这个判断就会失效,错误被吞掉,然后你拿到一个内容是错误信息的「图片」。

最终的解法:

  1. 按文件头识别真实格式,不要靠接口文档
  2. 非 PNG 的结果落盘前用 GD 转换一次
  3. 统一用自己封装的 wx_image_open() 读图
  4. 降级原因写进 $GLOBALS['wx_bill_error'],方便排查

延伸一条通用经验:「多个不同的故障现象对应同一个笼统的错误」往往意味着错误被中间层吞了,要去检查中间层而不是表层。

坑 4:类方法加错位置,服务器报 undefined method

core/wechat.php 里有三个类:WeChatClient、WxappAccount、WeAccount。

我给 WeChatClient 加方法时,用 grep 找的锚点恰好落在 WeAccount 的范围里,结果方法被插进了错误的类。本地测不出来(因为本地没跑全链路),传到服务器直接 undefined method。

教训:改类的结构后,必须用反射实际验证方法归属:

$r = new ReflectionClass('WeChatClient');
var_dump($r->hasMethod('fetchOH5User'));

只 grep 字符串是不够的。

坑 5:登录态死循环(这个和微信审核直接相关)

这个坑最值得说,因为它是「代码看起来完全正确、但用户用起来就是错」的典型。

小程序端有个登录检查的收口函数,逻辑是:

var u = wx.getStorageSync("userInfo");
if (u && u.sessionid) {
    cb();                    // 已登录,继续
} else {
    wx.navigateTo({ url: "../auth/auth" });   // 没登录,去授权页
}

看起来没问题。但问题是它被 8 个页面的 onShow 调用,包括「我的」这个 tab 页。

于是用户的真实体验是:

进入「我的」→ 没登录 → 跳授权页
   ↓
用户点左上角返回(不想授权)
   ↓
回到「我的」→ onShow 再次触发 → 又跳授权页
   ↓
用户再点返回 → 又跳……无限循环

微信审核直接驳回:「小程序登录环节,点击取消/返回选项无效,属于登录规范不合规。」

而且授权页本身连一个取消按钮都没有——设计的时候只考虑了「用户一定会登录」,没考虑「用户可能不想登录」。

修复方案我用了三层机制:

第一层:拒绝标记

授权页 onUnload 时判断——如果是「非登录成功」离开(点返回键、手势返回都算),写入一个 auth_denied 标记。收口函数读到这个标记后,就不再自动弹授权页。

onUnload: function() {
    if (!this._authOk) {              // 登录成功才会置 _authOk
        wx.setStorageSync("auth_denied", 1);
    }
}

登录成功路径要打标记豁免,否则刚登录完又被当成「拒绝」。

第二层:区分「自动检查」和「用户主动点击」

onShow 里的自动检查:已拒绝过就静默放行,什么都不做。
但用户主动点「通知车主」这类功能按钮时,不能静默——要弹窗询问:

此功能需要登录后使用,是否前往登录? [去登录] [暂不]

这样既不会骚扰用户,功能也不会「点了没反应」。

第三层:冷启动重置

app.js 的 onLaunch 里清除标记。这样用户下次打开小程序,仍然会正常引导一次登录——不能因为曾经拒绝过就永远不再提示,那用户体验也不对。

同时在授权页加了「暂不登录,先看看」的取消按钮,让「取消」这个动作显式存在。

改完之后我又顺手处理了一个同源的问题:并发登录导致的无限互踢。

原来的会话创建逻辑是「单会话模型」——每次登录都删掉该用户已有的所有会话。但小程序页面并发发起 4 个请求时,每个请求遇到 401 都会各自触发一次重新登录,每次重登都踢掉别人正在用的会话,于是各自又 401,再重登……30 秒内产生了 185 次登录请求,表现就是「授权登录一直转圈」。

改成「多会话并存,只清理已过期的会话」就解决了。配合前端加固:登录类接口自己报未登录时不再触发登录、自动重登封顶 1 次、登录请求单飞。

四、还有几个设计上的小细节

关于短信降级

前面提到微信订阅消息额度不可查询。所以我在短信通道未配置或发送失败时,接口会返回一个「客户端模式」的响应,前端拿到后弹窗让路人复制文案自己发短信。不写挪车记录、不限流——因为这条路径本来就不消耗我们任何资源,不该限制用户。

关于图标字体的保护

后台用了三种图标字体(zmdi、fa、we7icon)。我后来加了个「后台字体切换」功能,用户能选微软雅黑、宋体、楷体这些。字体覆盖 CSS 必须精确限定选择器范围,绝不能波及图标字体——否则整个后台的图标全变成方块。

关于移动端日志分析

有次排查线上问题,发现日志里混进了一堆奇怪的请求。后来意识到是我自己的探针(服务器本机和 curl 的 UA)。

所以现在分析日志前先按 IP + UA 过滤掉自己发的请求,再看真实设备的数据。另外一点:不要用响应字节数反推错误内容——同一个错误在不同版本、不同客户端下字节数会变,猜出来的结论经常是错的。

五、源码包说明

包里有这些:

白黎挪车_v1.3.9_源码分享包/
├── 安装说明.txt          安装部署全过程(含常见问题)
├── 源码说明.txt          程序结构、功能清单、数据库表、二次开发指引
├── 独立版/               后端 PHP(上传到站点根目录)
└── wxapp/                小程序前端(微信开发者工具打开)

环境要求

  • 服务器:宝塔面板 + Nginx + PHP 7.4(兼容 5.6~8.0)
  • PHP 扩展:pdo_mysql、curl、gd、openssl、mbstring、fileinfo、zip
  • 数据库:MySQL 5.7 / 8.0
  • 域名:需要 HTTPS(微信小程序强制要求)

部署大概三步

  1. 宝塔建站 → 上传 独立版/ 里的全部文件到站点根目录 → 访问 install.php 按向导填写 → 装完立刻删掉 install.php
  2. 改 wxapp/siteinfo.js 的 siteroot 为你自己的域名(必须以 /app/index.php 结尾)
  3. 微信开发者工具打开 wxapp/,填自己的 AppID,编译上传

详细步骤看包里的 安装说明.txt,那份写得比较细。

关于这个包的安全性

打包的时候我做了处理,可以放心:

  • 已删除 config.php 等私密配置(装完由 install.php 重新生成)
  • 小程序站点地址和 AppID 都换成了占位符
  • 清除了编译缓存、开发探针、一次性脚本、备份文件
  • 包内不含任何真实密钥、密码、域名

装好之后记得立刻改掉后台密码、填自己的小程序密钥。

六、最后说几句

这次剥离最大的收获不是技术本身,而是意识到:很多「看起来能跑」的代码,其实藏着只在特定路径下才暴露的问题。

像登录循环那个 bug,你在开发者工具里点一遍完整流程(登录成功)完全测不出来——因为它只在「用户拒绝登录」这条路径上出现。而这条路径恰恰是微信审核一定会走的。

所以如果这个包对你有用,装完之后请务必走一遍「审核路径」:

进「我的」→ 自动进授权页 → 点「暂不登录」→ 应该正常回到「我的」并显示未登录提示 → 点「点击登录」→ 正常授权

另外,如果有人打算把它装到对外运营的环境,建议把参数配置里的短信通道、虚拟号平台都配好。默认不配也能跑(走客户端降级),但体验会打折扣。腾讯地图 Key 这块特殊一点:后台默认没有这个配置项(原版把「地图相关」面板隐藏了),不配也能跑,扫码通知里的「位置」会显示为「附近」;想显示真实地址,放出该面板填一个免费的腾讯位置服务 Key 即可,安装教程 6.1 有具体步骤。

七、小程序演示

图片[1]-白黎挪车 v1.3.9 独立版:把微擎模块彻底剥离成独立 PHP,附完整源码分享-Baili Blog

源码包在文末下载,有问题欢迎留言交流。

© 版权声明
THE END
喜欢就支持一下吧
点赞17 分享
白黎挪车 v1.3.9 独立版:把微擎模块彻底剥离成独立 PHP,附完整源码分享-Baili Blog
白黎挪车 v1.3.9 独立版:把微擎模块彻底剥离成独立 PHP,附完整源码分享
此内容为付费资源,请付费后查看
¥99
限时特惠
¥199
立即购买
您当前未登录!建议登陆后购买,可保存购买订单
付费资源
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容