简介:面向帝国CMS 7.5 UTF8站点运营者与开发者的微信分享增强源码包,重点解决文章转发到朋友圈、发送给好友时缺少缩略图与简介的问题,适用于已认证订阅号或服务号、域名完成备案且PHP空间支持HTTPS抓取的建站场景。包内共6个文件,约31KB,以2个PHP接口文件、2个TXT说明与参数备注、1个JPG配置示意图和1个微信JS-SDK脚本组成,覆盖公众号凭证配置、分享接口调用及HTTPS环境下的脚本本地化等环节。已有1040人学习。其价值在于把认证公众号的appid、appsecret、JS接口安全域名和IP白名单等关键配置集中到可直接部署的目录中,并附access_token获取检测思路,便于快速判断配置是否生效;对需要排查分享卡片不显示、缩略图缺失或HTTPS抓取异常的中级建站人员较有参考意义,GBK版本需自行转码。
1. 分享卡片只有一个灰方块:这份源码补的是哪块短板
把帝国CMS 的文章链接丢进微信对话框,卡片上只有个灰底方块,标题后面挂着栏目名,简介那一栏空着——这个场景我见过太多次。站点里 titlepic 明明传了封面,正文开头也写了摘要,页面 head 里 og:image、description 一个不落,微信就是不认。原因不复杂:微信分享走的是 JS-SDK,页面标签它压根不读,它要的是当前页 URL 的一份签名,再用 updateShareData 把缩略图、简介、链接塞进去。
这份源码做的就是把这根线接通:帝国CMS 侧加一个签名接口和两级缓存,内容页模板把 titlepic、smalltext 取出来输出成一段 JSON,前端统一挂 wx.config。它适合还在维护帝国CMS 老站、后台模型和模板都不方便大改、只想让转发卡片正常显示缩略图和简介的人。签名怎么算、缓存怎么存、模板里字段怎么兜底,这套思路换到别的 CMS 上也一样能用。
2. 链路拆解:一次转发要过模板、接口、签名三道手
微信分享这条链路其实很短,短到很多人第一次做会以为「加个 meta 就完事」,结果折腾一下午。完整走一遍是:用户打开文章页 → 模板渲染时把标题、简介、封面图写进一段全局变量 → 前端 JS 拿当前页 URL 去请求自己的签名接口 → 接口返回 appId、timestamp、nonceStr、signature → wx.config 校验通过 → wx.ready 里调 updateShareData 塞数据。任何一环错位,最后都是「卡片没图」这一个现象,所以排查时不能只盯着模板。
下面按源码实际落位顺序拆,先是文件放哪,再是后端怎么算签名,最后是前端什么时机调。
2.1 源码落位:为什么放 /e/extend 而不是模板目录
帝国CMS 的目录结构里,/e/是程序主体,/e/class/放公共函数和类,/e/data/放缓存和临时文件,/e/extend/是官方留出来的扩展目录,模板则存在后台数据库或者/e/template/下。这份源码我建议整个丢进/e/extend/wxshare/,理由有三条:升级帝国CMS 时/e/extend/不会被覆盖;能直接require到/e/class/connect.php里的函数,比如截字用的esub();接口 URL 固定成/e/extend/wxshare/sign.php,前端写死也好、做域名白名单也好,都好处理。
/e/extend/wxshare/ ├── config.php # appId、appSecret、允许签名的域名白名单 ├── WxJsSdk.php # 签名类,含 token / ticket 两级缓存 ├── sign.php # 对外的签名接口,返回 JSON ├── share.js # 前端注入脚本,wx.config + updateShareData └── cache/ # 缓存目录,必须可写,建议加 .htaccess 禁直接访问cache/这个目录是重点,很多人图省事直接放/e/data/tmp/,那个目录帝国CMS 自己也在读写,清理缓存时容易连你的 token 一起删掉,第二天早上第一批访问的用户就吃到一次接口报错。单独建目录,权限给 755、属主跟 PHP 进程一致,config.php里只留配置不写逻辑,这样后面换公众号只改一个文件。
模板侧不用大动,内容页模板(一般是showtemplate那套)里加一段 PHP 和一个<script>,把文章数据吐给前端就行,具体写法放到第 3 章。这里先记住一个原则:模板只负责「给数据」,不负责「算签名」。签名必须放服务端,appSecret 绝对不能出现在 HTML 里,这是血泪经验,泄露一次就得去后台重置。
2.2 签名接口:access_token 和 jsapi_ticket 的两级缓存
签名公式本身一行就够:把jsapi_ticket、noncestr、timestamp、url按字段名字典序拼成 query string,做一次 sha1。真正麻烦的是jsapi_ticket从哪来——它得用access_token去换,而access_token每天有调用次数上限,多进程并发下还容易互相把对方刷失效。所以合格的做法是两级缓存:token 缓一份,ticket 缓一份,都带过期时间。
<?php // /e/extend/wxshare/WxJsSdk.php class WxJsSdk { private $appId; private $appSecret; private $cacheDir; public function __construct($appId, $appSecret, $cacheDir) { $this->appId = $appId; $this->appSecret = $appSecret; $this->cacheDir = rtrim($cacheDir, '/') . '/'; } // 第一级:access_token,有效期 7200 秒,提前 300 秒判过期 private function getAccessToken() { $file = $this->cacheDir . 'token_' . md5($this->appId) . '.php'; $data = $this->readCache($file); if ($data && $data['expire'] > time() + 300) { return $data['value']; } $url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential' . '&appid=' . $this->appId . '&secret=' . $this->appSecret; $res = json_decode($this->httpGet($url), true); if (empty($res['access_token'])) { return false; // 上层记录日志,不要把错误吞掉 } $this->writeCache($file, [ 'value' => $res['access_token'], 'expire' => time() + $res['expires_in'], ]); return $res['access_token']; } // 第二级:jsapi_ticket,同样 7200 秒 private function getJsapiTicket() { $file = $this->cacheDir . 'ticket_' . md5($this->appId) . '.php'; $data = $this->readCache($file); if ($data && $data['expire'] > time() + 300) { return $data['value']; } $token = $this->getAccessToken(); if (!$token) { return false; } $url = 'https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=' . $token . '&type=jsapi'; $res = json_decode($this->httpGet($url), true); if (empty($res['ticket'])) { return false; } $this->writeCache($file, [ 'value' => $res['ticket'], 'expire' => time() + $res['expires_in'], ]); return $res['ticket']; } // 对外只暴露这一个方法,返回 wx.config 需要的四个参数 public function buildSignPackage($url) { $ticket = $this->getJsapiTicket(); if (!$ticket) { return false; } $nonceStr = substr(md5(uniqid(mt_rand(), true)), 0, 16); $timestamp = time(); // 字段名字典序拼接,url 用原始未编码的完整地址,不要 urlencode $raw = 'jsapi_ticket=' . $ticket . '&noncestr=' . $nonceStr . '×tamp=' . $timestamp . '&url=' . $url; return [ 'appId' => $this->appId, 'timestamp' => $timestamp, 'nonceStr' => $nonceStr, 'signature' => sha1($raw), ]; } private function readCache($file) { return is_file($file) ? include $file : false; } private function writeCache($file, $payload) { // 先写临时文件再 rename,避免并发读到写了一半的内容 $tmp = $file . '.' . getmypid() . '.tmp'; file_put_contents($tmp, '<?php return ' . var_export($payload, true) . ';'); rename($tmp, $file); } private function httpGet($url) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => true, ]); $out = curl_exec($ch); curl_close($ch); return $out; } }几个参数值得单独说清楚。expire > time() + 300里的 300 秒是提前量——微信的 token 是「拿到那一刻起算 7200 秒」,但网络延迟、服务器时钟偏差都会让边界判断飘,不留提前量就会出现「刚判断没过期、拿去请求就报 40001」。CURLOPT_TIMEOUT => 5是必须的,微信那边偶发慢响应,不设超时会把 PHP-FPM 进程挂住,文章页跟着变慢。
writeCache里先写.tmp再rename这一步容易被忽略。rename在同一文件系统内是原子操作,读者要么看到旧文件要么看到新文件,不会读到半截 PHP 语法错误的白屏。我在并发压测时踩过这个坑:几十个请求同时进来都发现缓存过期,一起写同一个文件,其中几个读到空文件直接 fatal error。
拼签名时url一定不要urlencode。字典序拼接阶段用的是原始字符串,编码是最后传给前端做 query 参数时才做的事,两处搞混是 invalid signature 的高发原因。
2.3 前端时序:wx.config 必须在分享数据之前
后端算完签名,接口文件把结果吐成 JSON。这里加一层域名白名单,避免这个接口被人当成「任意 URL 签名器」拿去乱用:
<?php // /e/extend/wxshare/sign.php require __DIR__ . '/WxJsSdk.php'; require __DIR__ . '/config.php'; // 里面定义 $appId / $appSecret / $allowHosts header('Content-Type: application/json; charset=utf-8'); $url = isset($_GET['url']) ? trim($_GET['url']) : ''; $host = parse_url($url, PHP_URL_HOST); if (!$url || !in_array($host, $allowHosts, true)) { echo json_encode(['code' => 1, 'msg' => 'bad url']); exit; } // 微信签名不吃 # 后面的部分,先切掉 $url = strtok($url, '#'); $sdk = new WxJsSdk($appId, $appSecret, __DIR__ . '/cache'); $pkg = $sdk->buildSignPackage($url); if (!$pkg) { echo json_encode(['code' => 1, 'msg' => 'sign fail']); exit; } echo json_encode($pkg, JSON_UNESCAPED_UNICODE);前端拿到这四个参数后,顺序不能乱:先wx.config,成功回调wx.ready里再设置分享内容。而且jsApiList要把用到的接口都列进去,漏一个就是调用无反应、控制台还不报错。
// /e/extend/wxshare/share.js (function () { if (!window.__SHARE__) return; var s = window.__SHARE__; var signUrl = location.href.split('#')[0]; // 与后端保持同一份 URL fetch('/e/extend/wxshare/sign.php?url=' + encodeURIComponent(signUrl)) .then(function (r) { return r.json(); }) .then(function (cfg) { if (cfg.code === 1) { console.warn('[wx] sign fail', cfg.msg); return; } wx.config({ debug: false, appId: cfg.appId, timestamp: cfg.timestamp, nonceStr: cfg.nonceStr, signature: cfg.signature, jsApiList: [ 'updateAppendShareData', 'updateShareData', 'onMenuShareTimeline', 'onMenuShareAppMessage' ] }); wx.ready(function () { var data = { title: s.title, desc: s.desc, link: s.link, imgUrl: s.img }; // 新版 JSSDK 走这个 if (wx.updateShareData) { wx.updateShareData(data); } // 老接口兜底,部分客户端仍认 if (wx.onMenuShareTimeline) { wx.onMenuShareTimeline({ title: data.title, link: data.link, imgUrl: data.imgUrl }); } if (wx.onMenuShareAppMessage) { wx.onMenuShareAppMessage({ title: data.title, desc: data.desc, link: data.link, imgUrl: data.imgUrl }); } }); wx.error(function (res) { console.warn('[wx] config error', res); }); }); })();signUrl必须和用户地址栏里看到的完全一致——带不带www、末尾有没有斜杠、query 参数顺序,全都算在内,差一个字符就是 invalid signature。老站点经常同时存在http://和https://两套入口,这种要在服务器层面 301 收敛到一个,不然签名永远对不上。如果站点是纯静态缓存页(帝国CMS 开了静态生成),location.href取的是当前真实地址,反而是好事,不用额外处理。
3. 正文页改造:把 titlepic 和 smalltext 真正喂进分享卡片
后端接口通了,接下来是数据源。帝国CMS 内容页模板里能拿到当前文章的$navinfor数组,title、titlepic(标题图)、smalltext(简介)、newstext(正文)都在里面。理论上直接取就行,实际上有一半的文章 titlepic 是空的,简介字段编辑嫌麻烦没填,封面图存的是相对路径,正文里的图还带防盗链。这一章就是把这些脏数据磨平。
3.1 模板取字段:titlepic 空了拿什么兜底
先在后台把「模板支持程序代码」打开,否则模板里的<?php ?>会被原样输出。然后内容页模板里加这么一段,放在</head>之前或者 body 末尾都行,注意要在引入 share.js 之前。
<?php // 内容页模板内,$navinfor 为当前文章数组 $shareImg = trim($navinfor['titlepic']); $shareDesc = trim(strip_tags($navinfor['smalltext'])); // 兜底一:没有标题图,从正文抓第一张 img if ($shareImg === '' && preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $navinfor['newstext'], $m)) { $shareImg = $m[1]; } // 兜底二:还是没有,用栏目配置的默认图 if ($shareImg === '') { $shareImg = '/e/extend/wxshare/default.jpg'; } // 兜底三:简介为空,截正文前 60 字 if ($shareDesc === '') { $shareDesc = esub(strip_tags($navinfor['newstext']), 60); } // 相对路径补成绝对地址 if (strpos($shareImg, 'http') !== 0) { $shareImg = 'https://' . $_SERVER['HTTP_HOST'] . '/' . ltrim($shareImg, '/'); } ?> <script> window.__SHARE__ = { title: <?= json_encode($navinfor['title'], JSON_UNESCAPED_UNICODE) ?>, desc : <?= json_encode($shareDesc, JSON_UNESCAPED_UNICODE) ?>, img : <?= json_encode($shareImg, JSON_UNESCAPED_UNICODE) ?>, link : <?= json_encode('https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], JSON_UNESCAPED_UNICODE) ?> }; </script> <script src="/e/extend/wxshare/share.js"></script>esub()是帝国CMS 自带的截字函数,第二个参数是字符数,中文按字符算,不用自己写 mb_substr。json_encode后面加JSON_UNESCAPED_UNICODE是为了中文不被转成\uXXXX,虽然不影响功能,但调试时看输出会舒服很多。
三个兜底顺序是有讲究的:正文首图优先于默认图,因为首图跟内容相关,点开率明显高;默认图只在前两者都没有时出场,别小看它——一篇文章分享出去是灰方块,用户基本不会点,有个栏目统一配图至少卡片是完整的。这里踩过一次坑:正文首图抓的是<img src="data:image/...">懒加载占位图,分享出去显示一个 1×1 透明像素,看起来跟没图一样。加一条strpos($shareImg, 'data:') === 0就跳过。
另外,$navinfor['title']里如果带了站点名后缀(很多模板会拼),分享标题就会变成「文章标题 - 站点名 - 站点名」。在模板里取原始$navinfor['title']而不是$navinfor['title']经过处理的版本,或者干脆在 share.js 里做一次截断,控制在 30 字以内,微信卡片标题太长会被省略号截掉,反而不好看。
3.2 图片地址归一化:相对路径、附件域名和混合内容
帝国CMS 的附件路径取决于后台「附件地址」设置。有的站是相对路径/d/file/xxx.jpg,有的是独立附件域名http://img.example.com/...(这里只作格式示意),还有的开了 https 但数据库里存的历史图片还是 http。微信客户端对分享缩略图的要求,经验值是这样的:正方形、300×300 以上、文件控制在几十 KB 以内最稳,非正方形会被裁切,太大或格式冷门(比如 webp 在某些旧版本上)可能直接不显示。
归一化这一步建议单独写个函数,别塞模板里:
<?php // /e/extend/wxshare/helper.php function wx_abs_url($path, $scheme = 'https') { $path = trim($path); if ($path === '') return ''; if (strpos($path, 'data:') === 0) return ''; // 懒加载占位图 if (stripos($path, 'http://') === 0) { // 站点全站 https 时,把 http 图片也升上去,避免混合内容被拦 return $scheme . substr($path, 4); } if (stripos($path, 'https://') === 0) return $path; // 协议相对地址 //img.xxx.com/a.jpg if (strpos($path, '//') === 0) return $scheme . ':' . $path; return $scheme . '://' . $_SERVER['HTTP_HOST'] . '/' . ltrim($path, '/'); }参数说明:$path是数据库里存的原始值,$scheme默认 https,是因为绝大多数公众号要求 JS 接口安全域名走 https。这个函数只做地址层面的改写,不判断图片是否存在——判断存在性要发 HTTP 请求,放在模板渲染里会拖慢页面。真要做好,是在文章发布时用钩子异步检测一次,把不合格的图记进日志,让编辑去换。
防盗链是另一个高频问题。图片站常开 Referer 白名单,微信客户端拉图时 Referer 为空或者不是你的域名,直接 403,卡片就成了灰块。解决方式要么给缩略图走一个不带防盗链的独立域名,要么在 Nginx 里对/d/file/下特定尺寸的缩略图放开空 Referer。这条别偷懒,测试时一定要用真手机发一次,浏览器里看是正常的,微信里不一定。
3.3 分享链接与回流统计:把 from 参数接回自己的计数
分享出去的链接默认就是当前页 URL,好处是签名对得上,坏处是你不知道流量从哪来。常见做法是在link里追加一个标记参数,比如?fx=1,然后在文章页入口处判断这个参数,往自己的统计表里写一条。注意:link里的 URL 和签名用的 URL 是两回事,前者可以带参数,后者必须是当前真实地址,别把两者搞混。
<?php // 文章页顶部,统计分享回流 if (isset($_GET['fx'])) { $aid = intval($navinfor['id']); $src = $_GET['fx'] === '2' ? 'timeline' : 'friend'; // 简单写一张自建统计表,或用帝国CMS 的点击数表扩展 $empire->query("INSERT INTO {$dbtbpre}ecms_share_log(aid, src, ip, addtime) VALUES ('$aid', '$src', '" . egetip() . "', '" . time() . "')"); }$empire是帝国CMS 的数据库操作对象,$dbtbpre是表前缀,egetip()取真实 IP(会读X-Forwarded-For,前面有 CDN 时记得配置可信代理)。这张表不用建索引太多,量大了按天分表或者只留最近 30 天,不然文章表没垮,日志表先撑爆。
想让统计更准,可以在 share.js 里给朋友圈和好友分别传不同的link:wx.updateShareData是统一设置,onMenuShareTimeline和onMenuShareAppMessage可以各传各的,正好用来区分两个入口。
4. 配置与缓存:让签名接口在流量峰值下不打爆配额
签名接口是每打开一篇文章就要调一次的,遇上热点文章被大号转发,QPS 能翻好几倍。这个接口本身很轻(读缓存 + sha1),真正怕的是缓存失效瞬间的大量回源——几百个请求同时发现 ticket 过期,一起去找微信换新的,轻则触发频率限制,重则把刚拿到的 token 刷失效,进入「一直失败一直重试」的死循环。
4.1 文件缓存还是数据表:并发下的两种写法
小站用文件缓存足够,成本低、不用动数据库。多机部署就得换成共享存储,常见选择是 Redis 或者数据库表。用帝国CMS 自带的数据库操作最省事,建一张两列的表就行:
CREATE TABLE IF NOT EXISTS `phome_wx_cache` ( `ckey` varchar(64) NOT NULL COMMENT '缓存键,如 ticket_abc123', `cvalue` text NOT NULL COMMENT 'access_token 或 jsapi_ticket', `expire` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '绝对过期时间戳', PRIMARY KEY (`ckey`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8;phome_是帝国CMS 默认表前缀,装站时改过就换成自己的。ckey用token_/ticket_加 appId 的 md5,一个库里挂多个公众号也不会串。expire存绝对时间戳而不是剩余秒数,避免不同机器时钟不一致算出不同结果。
换成数据库后,并发问题从「同时写文件」变成「同时回源」。加一把锁最直接:更新前先UPDATE ... SET expire = 过期时间 WHERE ckey = 'x' AND expire < 当前时间,影响行数为 1 的进程才去调微信接口,其他进程直接读旧值或者短暂重试。这招在文件缓存上也能用——用flock()排他锁包住回源那一段,代价是请求会短暂排队,但比一起打微信接口强。
提示:缓存目录和缓存表都要有清理策略。token 泄露或者换了 appSecret 之后,旧缓存不清会一直报 40001,很多人第一反应是代码写错了,其实清一下缓存就好。
4.2 后台配置项与公众号后台的对应关系
签名失败有一半不是代码问题,是配置对不上。这张表是我每次接新站都要对着核一遍的:
| 配置项 | 写在哪 | 对应公众号后台位置 | 常见错误 |
|---|---|---|---|
| appId | config.php | 设置与开发 - 基本配置 | 复制时带了空格 |
| appSecret | config.php | 同上,重置后旧值立即失效 | 重置后忘了改文件 |
| JS 接口安全域名 | 公众号后台 | 设置与开发 - 公众号设置 - 功能设置 | 带了https://或端口 |
| 域名校验文件 | 网站根目录 | 同上,下载 MP_verify_xxx.txt | 放错目录或返回 404 |
| 服务器出口 IP | 公众号后台 | 基本配置 - IP 白名单 | 只填了入口 IP,漏了出口 |
几个细节:安全域名不能带协议头和端口,填www.example.com这种裸域名(这里仅示意格式);校验文件必须能从根域名直接访问到,帝国CMS 有些站的根目录被伪静态规则拦了.txt,得单独加一条location ~* \.txt$ { try_files $uri =404; }之类的放行。IP 白名单是出口 IP,不是域名解析的 IP——如果服务器走 NAT 出去,得问运维要真实出口地址,填错了报 40164。
多域名站点(PC 站 + 移动站)要在安全域名里把所有用到的域名都加上,最多三个。mobile 站如果是独立域名,模板里取$_SERVER['HTTP_HOST']生成link是对的,但签名接口那边一定要按请求的实际 host 来签,别写死一个。
5. 避坑排查:上线后最常撞上的五类转发异常
调这块最难受的地方是失败现象高度相似——九成都是「卡片没图」或者「简介不对」,但原因可能在南辕北辙的五个地方。下面这五条按我遇到的频率排序,每条都按现象、原因、解决三步写,照着对一遍基本能定位。
5.1 图有了,简介变成正文前几十个字
现象:分享卡片缩略图正常,但简介显示的是正文开头那段,而不是后台填的摘要。
原因:模板取smalltext时这个字段是空的,走了兜底逻辑截正文。帝国CMS 的smalltext需要在后台「录入项」里勾选并填写,很多老站从别的系统迁移过来,这个字段压根没数据;还有一种情况是模板里取成了$navinfor['smalltext']的 HTML 版本,里面带了<p>标签,微信把标签当文本截断了。
解决:先确认字段有没有值,没有就推动编辑补,别长期靠截正文兜底。有值但带标签的,用strip_tags()过一遍再trim(),连续空格用preg_replace('/\s+/u', ' ', $str)压成一个。截断长度控制在 40 到 60 字之间,微信朋友圈卡片一行大概能放 14 个字,两行刚好。
5.2 iOS 上正常,安卓上分享出去还是老样子
现象:iPhone 转发出去是新卡片,安卓手机转发还是旧的标题和灰图,清缓存也不行。
原因:安卓微信客户端对分享数据的读取时机和 iOS 不一样,wx.config没走完就点了右上角菜单,读到的还是默认值;另一种情况是站点开了页面缓存(比如 CDN 缓存了 HTML),window.__SHARE__里还是上一版数据。
解决:把wx.ready里的设置逻辑提前到页面加载完成就执行,别等到用户点菜单才设。同时给签名接口的响应加Cache-Control: no-store,HTML 页面如果走 CDN,对?fx=这类参数做不缓存规则。测试时不要只在开发者工具里看,必须真机,iOS 和安卓各测一台。
5.3 控制台报 invalid signature
现象:wx.config回调wx.error,提示签名错误,卡片完全不生效。
原因:按出现频率排,一是 URL 不一致(多了www、少了末尾斜杠、http和https混用);二是签名前对 url 做了urlencode;三是jsapi_ticket缓存串了别的公众号的值;四是服务器时间不准,timestamp和微信侧差太多。
解决:在sign.php里把参与签名的$url原样error_log下来,跟手机浏览器地址栏逐个字符比对。写个临时调试页,把sha1的原文打印出来,用微信官方提供的校验工具对一遍。时间问题用ntpdate同步一下,或者直接在服务器上date看偏差。
5.4 图片 403,浏览器能打开微信打不开
现象:缩略图地址直接在浏览器里访问正常,微信里显示灰块,抓包看是 403。
原因:图片服务器开了防盗链,微信客户端拉图时 Referer 为空或不是白名单域名;也可能是图片走的是 http,页面是 https,混合内容被客户端拦了。
解决:给缩略图单独放行空 Referer,或者在 Nginx 里对图片来源做一个不受防盗链限制的路径。http 图片统一在wx_abs_url()里升到 https,前提是图片服务器支持 https,不支持就得换图床或者加证书。
5.5 定时任务跑完,全天分享都失效
现象:每天凌晨过后一段时间,所有文章的分享卡片全部没图,过一阵又自己好了。
原因:有人写了个定时任务「每天清一次 access_token 缓存」,本意是防止过期,结果清完之后大量请求同时回源,微信侧把新 token 刷掉,旧 token 又已经失效,中间这段时间全是失败。
解决:删掉这种「定时清缓存」的任务,靠expire自然过期加提前量就够了。要预热就在低峰期跑一个只读任务,先请求一次sign.php把缓存填上,不要去删。真的需要强制刷新,加一个带密钥的手动接口,人工触发。
6. 二次开发:从单篇文章扩到全站,再让卡片带上栏目和作者
单篇改造做完,接下来八成会遇到「全站都要生效」的需求。最省事的做法不是每套模板都贴一遍代码,而是把数据组装抽成一个函数,放在/e/extend/wxshare/helper.php里,模板只留一行调用:
<?php // helper.php 里补充:统一组装分享数据 function wx_share_data($navinfor, $defaultImg = '/e/extend/wxshare/default.jpg') { $title = trim($navinfor['title']); // 标题带栏目名,方便订阅号列表里区分来源 if (!empty($navinfor['classname'])) { $title = $title . ' | ' . $navinfor['classname']; } $img = wx_abs_url($navinfor['titlepic']); if ($img === '' && preg_match('/<img[^>]+src=["\']([^"\']+)["\']/i', $navinfor['newstext'], $m)) { $img = wx_abs_url($m[1]); } if ($img === '') $img = $defaultImg; $desc = trim(strip_tags($navinfor['smalltext'])); if ($desc === '') $desc = esub(strip_tags($navinfor['newstext']), 50); return [ 'title' => esub($title, 30), 'desc' => $desc, 'img' => $img, 'link' => 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'], ]; }模板里就变成window.__SHARE__ = <?= json_encode(wx_share_data($navinfor), JSON_UNESCAPED_UNICODE) ?>;,栏目页、专题页也能复用,传进去的数组凑齐title、titlepic、smalltext、newstext四个键就行。
再往前一步,是让不同入口的卡片有差异。列表页分享和详情页分享,标题策略不一样:详情页用文章标题,列表页用「栏目名 + 最新更新」,用户点进来预期不同。用updateAppendShareData可以在已有的分享数据上追加字段(部分版本支持),但更稳的还是按页面类型渲染不同的__SHARE__,前端只做一次读取。
| 改造范围 | 改动点 | 工作量 | 适合场景 |
|---|---|---|---|
| 单篇模板 | 模板加 PHP + 引 share.js | 半小时 | 只想修好内容页 |
| 全站函数化 | 抽 helper + 各模板调用 | 半天 | 栏目页、专题页也要 |
| 加统计回流 | link 带 fx 参数 + 日志表 | 一天 | 要看分享带来的量 |
| 多公众号 | 配置按域名分组 | 一天 | 一站多号运营 |
验证方法别偷懒:改完之后用微信开发者工具的「公众号网页调试」先看wx.config是否 ok,再用两台真机分别发一次好友和朋友圈,确认标题、简介、图三样都对。有条件的话在分享链接上带一个明显的短参数,看服务端日志里有没有收到,能同时验证回流统计通没通。
说个我自己的教训。早期做这类改造,我习惯在浏览器里看着卡片正常就交付,结果上线第二天运营反馈安卓端全是灰图。从那以后我每次动分享相关的代码,都强制走一遍「iOS 真机发好友 → 安卓真机发朋友圈 → 换一个没登录过的微信再发一次」这三步,一步不落。缓存、域名、客户端差异这三样,少测一个都可能翻车。希望帮到你。
本文还有配套的精品资源,点击获取