写在前面
前阵子想给自己弄一套「扫码挪车」的小程序——车停在外面,别人需要挪车的时候,扫一下贴在挡风玻璃上的码,就能在不看到我真实手机号的情况下联系到我。
找了一圈现成方案,发现大部分都是微擎模块形态:功能是完整的,但必须装在微擎框架里才能跑。微擎本身是个挺重的系统,为了一个挪车功能去搭一整套框架,怎么想都不划算;而且真机跑起来一旦哪里出问题,排查链路会被拉得很长(框架层、模块层、还有各种授权校验)。
所以这段时间我做了一件事:把整个模块从微擎里完整剥离出来,改写成独立 PHP 程序。不依赖微擎,宝塔面板建个站、上传、装完就能跑,界面也做了一些调整。
整个过程踩了不少坑,也确实值得记录一下。文末附完整源码包(后端 + 小程序端),有需要的可以拿去用。
一、成品是什么样
先把功能说清楚,这样你能判断是不是自己需要的。
车主侧(小程序)
| 功能 | 说明 |
|---|---|
| 微信授权登录 | 支持官方头像昵称填写能力,也可以跳过 |
| 车主绑定 | 车牌号 + 手机号绑定,支持换绑 |
| 挪车码生成 | 批量生成、小程序码、圆形码成品图,可下载打印 |
| 码管理 | 查看、换绑、删除、放大保存相册 |
| 停车位置记录 | 反向找车用,手动记录停车位置 |
| 挪车记录 | 谁在什么时候扫了码、发了什么留言 |
| 订阅消息 | 微信挪车通知授权,带累计授权次数记账 |
| 隐私号/短信通知 | 两种通知通道都支持 |
| 带货 | 微信小店精选商品(可选) |
路人侧(扫码,完全不需要登录)
这是我觉得设计上最关键的一点:路人扫码进来不需要授权、不需要登录、不需要关注,直接就能看到车牌和通知按钮。真到了要挪车的场景,谁有耐心先授权登录再帮你挪车?
路人可以选三种方式联系车主:
- 隐私号外呼 —— 平台分配临时号码,双向都看不到真实手机号
- 平台发短信 —— 直接给车主手机发挪车短信
- 客户端降级 —— 短信通道没配或者额度用完时,弹出文案让路人用自己的手机发
第 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 头,这个判断就会失效,错误被吞掉,然后你拿到一个内容是错误信息的「图片」。
最终的解法:
- 按文件头识别真实格式,不要靠接口文档
- 非 PNG 的结果落盘前用 GD 转换一次
- 统一用自己封装的
wx_image_open()读图 - 降级原因写进
$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(微信小程序强制要求)
部署大概三步
- 宝塔建站 → 上传
独立版/里的全部文件到站点根目录 → 访问install.php按向导填写 → 装完立刻删掉 install.php - 改
wxapp/siteinfo.js的siteroot为你自己的域名(必须以/app/index.php结尾) - 微信开发者工具打开
wxapp/,填自己的 AppID,编译上传
详细步骤看包里的 安装说明.txt,那份写得比较细。
关于这个包的安全性
打包的时候我做了处理,可以放心:
- 已删除
config.php等私密配置(装完由install.php重新生成) - 小程序站点地址和 AppID 都换成了占位符
- 清除了编译缓存、开发探针、一次性脚本、备份文件
- 包内不含任何真实密钥、密码、域名
装好之后记得立刻改掉后台密码、填自己的小程序密钥。
六、最后说几句
这次剥离最大的收获不是技术本身,而是意识到:很多「看起来能跑」的代码,其实藏着只在特定路径下才暴露的问题。
像登录循环那个 bug,你在开发者工具里点一遍完整流程(登录成功)完全测不出来——因为它只在「用户拒绝登录」这条路径上出现。而这条路径恰恰是微信审核一定会走的。
所以如果这个包对你有用,装完之后请务必走一遍「审核路径」:
进「我的」→ 自动进授权页 → 点「暂不登录」→ 应该正常回到「我的」并显示未登录提示 → 点「点击登录」→ 正常授权
另外,如果有人打算把它装到对外运营的环境,建议把参数配置里的短信通道、虚拟号平台都配好。默认不配也能跑(走客户端降级),但体验会打折扣。腾讯地图 Key 这块特殊一点:后台默认没有这个配置项(原版把「地图相关」面板隐藏了),不配也能跑,扫码通知里的「位置」会显示为「附近」;想显示真实地址,放出该面板填一个免费的腾讯位置服务 Key 即可,安装教程 6.1 有具体步骤。
七、小程序演示
![图片[1]-白黎挪车 v1.3.9 独立版:把微擎模块彻底剥离成独立 PHP,附完整源码分享-Baili Blog](https://cdn.bailir.top/wp-content/uploads/img/2026/09/20260921171847482.jpg)
源码包在文末下载,有问题欢迎留言交流。
本站收集的资源仅供内部学习研究软件设计思想和原理使用,学习研究后请自觉删除,请勿传播,因未及时删除所造成的任何后果责任自负。
如果用于其他用途,请购买正版支持作者,谢谢!若您认为「Baili Blog」发布的内容若侵犯到您的权益,请联系站长邮箱:b71239135@gmail.com 进行删除处理。
本站资源大多存储在云盘,如发现链接失效,请联系我们,我们会第一时间更新。



















请登录后查看评论内容