ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

微信公众号自动发卡密:微擎模块实现关注、扫码、支付全场景领取

2026/9/15 6:45:37 拓冰建站 浏览量
微信公众号自动发卡密:微擎模块实现关注、扫码、支付全场景领取 简介这是一套完整的微信公众号‘关注送卡密’活动源码版本1.1.27定位为可直接部署的营销工具面向需要快速开展公众号粉丝增长、卡密分发与售卖的运营人员或开发者。代码覆盖关注自动发送、自定义菜单点击领取、扫码领取认证服务号等多入口发卡流程同时支持卡号密码模式——用户进入后发送卡号即可收到对应密码并支持支付收费售卖卡密及用户手机号获取。后台提供批量删除卡密与领取记录、活动起止时间控制、粉丝性别限制、新老粉丝限制、链接触发关键字等实用功能可灵活适配抽奖、会员兑换、付费发卡等场景。压缩包共27个文件、约199KB主体为后端逻辑与网页后台页面辅以脚本交互、图片、表格模板等结构清晰便于二次修改与部署测试。目前已有567人学习浏览适合作为公众号开发者研究卡密交互逻辑与活动页面的参考样例也能帮助运营人员快速搭建发卡活动。1. 为什么“关注送卡密”适合做成微信模块公众号源码里做关注送卡密看起来只是用户关注后自动回复一段文本但真正要在生产环境跑起来牵涉到消息路由、二维码场景值、粉丝属性、支付回调一整套链路。这个 1.1.27 版本的 ld_sendcode 模块把上述能力收敛进微擎模块的标准目录里在任何类型的公众号上都可以挂载不用碰公众号后台的开发者配置。适合三类人第一类是给企业和门店做私域运营的 PHP 工程师第二类是卖虚拟商品需要自动发货的个人站长第三类是手上已有公众号但不想写原生微信接口的运维。本文从代码结构一路讲到参数调优顺便把卡号密码模式、支付售卖、活动时间控制这些容易踩坑的地方都说清楚新手照着配置能上线熟手也能看到边界。2. ld_sendcode 模块文件结构与公众号消息路由2.1 从 manifest.xml 看模块声明微擎模块的入口是 manifest.xml它决定模块的版本、安装方式以及能订阅哪些消息类型。ld_sendcode 1.1.27 的 manifest.xml 核心内容长下面这样?xml version1.0 encodingUTF-8? module nameld_sendcode/name title关注送卡密/title version1.1.27/version typebusiness/type ability关注自动发送卡密、菜单点击领取、扫码领取、支付售卖/ability subscribes message typesubscribe / message typeclick / message typescan / message typetext / /subscribes handles message typesubscribe / message typeclick / message typescan / message typetext / /handles /module微擎通过subscribes决定把哪些公众号事件推送给模块通过handles决定模块能否在规则中接管这些消息。这里订阅了四种subscribe 对应关注和扫码关注click 对应菜单事件scan 对应扫码事件text 对应文本消息。如果只做关注送卡密把 click 和 scan 删掉也能跑但保留的好处是后台可以配置菜单点击领取和扫码领取不用改代码。参数说明type的 business 表示这是一个业务模块安装后出现在微擎后台的功能模块列表里。version不用多说但注意微擎升级模块时会和 manifest 里的版本号比对如果源码包是 1.1.27而数据库里已是 1.1.28降级安装会失败测试环境里这是常见问题。2.2 receiver.php、processor.php 与 site.php 的分工这里要区分三个文件很多第一次看微擎源码的人会被绕晕。文件作用触发时机receiver.php接收微信服务器推送的事件不返回回复用户关注、扫码、点击菜单时processor.php参与微擎的规则匹配处理文本消息用户发送文本关键字时site.php后台管理页面、支付回调、业务逻辑入口管理员配置、用户完成支付时receiver.php 里最典型的处理逻辑是这样public function receive() { $type $this-message[type]; if ($type subscribe) { $this-handleSubscribe(); } }注意 receive() 是微擎消息接收的统一入口$this-message是由微擎解析后的数组type字段区分 subscribe、scan、click、text。很多人在这里直接写 die() 或 exit()会把微擎的消息上下文打断导致后续没法记录日志正确做法是调用模块内部方法后返回空值把回复交给回复规则或直接在模块内用$this-response()。processor.php 处理文本关键字时需要实现respond()方法。常见写法是判断$this-message[content]是否等于后台配置的关键字命中后调用 site.php 里的发卡方法。注意 processor.php 不负责用户输入手机号后的二次匹配那是通过回复中的“进入卡密模式”引导用户再发一次卡号由 receiver 或 processor 的文本分支继续接管。site.php 的目录结构里会出现inc/和template/inc 下按模块方法拆文件例如inc/site/activity.php后台每个操作对应一个 doWeb 方法。excel目录或模板里的导出逻辑一般也在 site.php 中跳转所以这个文件是模块里最重的一个。2.3 手机号收集与 H5 页面的位置摘要里提到“用户输入手机号获取”这通常不是纯文本对话而是模块在回复内容中拼接一个 H5 链接。手机号页面可以放在模块的template/目录下也可以单独打包成 H5 项目用公众号网页授权获取 openid。源码包里的poster.class.php是海报生成器核心作用是把带场景值的二维码图片合成推广海报用户识别后进入扫码领取流程。$qr new Poster(); $qr-setScene(cardkey_ . $cardId); $qr-setExpire(2592000); $data $qr-create();这里的setScene是二维码参数字符集长度限制是 64 字节微信文档里说 32 个以内是数字超过就是字符串实测 1.1.27 版本里主要用的是字符串因为要带上卡密批次 ID。setExpire单位是秒2592000 正好是 30 天。海报里如果要塞头像昵称得在生成前调一次用户信息接口开发者模式下才有权限普通订阅号拿不到昵称时会显示默认占位图。3. 四种领取场景关注、菜单、扫码、关键字的实现与参数3.1 关注自动发送的开关与回复策略后台“关注自动送卡密”的配置项通常分成三个块开关、活动时间、回复内容。首先是开关只有打开开关receiver.php 里的 subscribe 分支才会真正调用发卡逻辑。我见过有人把开关打开后没有设置活动结束时间导致用户凌晨三点关注也收到卡密被羊毛党一晚刷走几百张所以时间控制必须有。if ($setting[enabled] 1) { $now time(); if ($setting[begin_time] $now $now $setting[end_time]) { $code $this-sendCode($openid, subscribe); } }sendCode内部会先检查粉丝性别和新老粉丝状态。老粉丝的判定方式有两个实现层次一个是查mc_members表里的注册时间另一个是看模块自己的ld_receiver_log里有没有历史领取记录。两种都常见区别在于前者只要关注过就算老粉丝后者需要真正领取过才算。做活动复购时建议用领取记录判断避免一个用户取关再关注后领不了第二次。文本回复的模板参数不只是卡密本身还支持{卡号}、{密码}、{有效期}、{剩余库存}这些占位符。注意微信被动回复最长 2048 字节如果模板里拼接了十条卡密直接超限所以 1.1.27 的默认策略是一条回复只带一张卡密需要多条的话走客服消息接口分批发但这要求粉丝在 48 小时内有互动否则发不出去。3.2 菜单点击领取的规则与限制菜单点击领取依赖公众号“自定义菜单”里的按钮按钮类型选择“发送消息”然后关联到模块创建的回复规则。微擎后台模块列表里找到 ld_sendcode点“进入模块”在“菜单领取”里创建规则系统会自动生成一个关键字比如ld_get_card再到公众号后台把菜单按钮事件指向这个关键字。这里最容易踩的坑是菜单使用“跳转网页”而不是“发送消息”。跳转网页只能进 H5 页面触发不了 click 事件自然走不到 receiver.php 的 click 分支。另一个坑是菜单发布后需要时间生效微信官方建议三到五分钟后再测试实际有时要等十分钟测试时可以先在公众号对话框手动输入菜单关键字验证逻辑是否通。菜单领取还有一种变体就是按钮事件触发后返回图片消息图片里带着卡密。这种方式需要提前把图片上传到微信素材库拿到 media_id 再配置到模块参数里。好处是防止卡密被长截图后搜索定位坏处是图片素材有有效期长期活动需要定时刷新 media_id而刷新素材后旧图片链接会失效历史聊天记录里的图片会变成无法加载的灰块。3.3 扫码领取认证服务号与临时二维码的配合扫码领取分为两种情况用户未关注时扫二维码会先进入关注流程事件类型是 subscribe但参数里带EventKey和Ticket已关注用户扫码事件类型是 scan不带 Ticket。receiver.php 必须把这两种情况都处理否则老用户扫码后没有任何反应。if ($type subscribe !empty($this-message[eventkey])) { $scene $this-message[eventkey]; } elseif ($type scan) { $scene $this-message[eventkey]; } $scene str_replace(qrscene_, , $scene);注意EventKey的值在未关注场景下前缀是qrscene_已关注场景下没有前缀。源码里如果不做前缀清理二维码参数会多出 8 个字符导致场景值对不上号。这个 bug 在自定义海报场景里尤其明显同一张海报老粉扫码领取成功新粉关注后领取失败日志里报的场景值一个是qrscene_cardkey_1001一个是cardkey_1001。扫码领取还依赖公众平台的“生成带参数二维码”接口认证服务号才有权限。模块后台一般会提供一个按钮一键生成当前活动的二维码。生成的二维码是临时二维码默认 30 天过期过期后旧海报全部失效。如果需要长期码得用QR_LIMIT_STR_SCENE但累计数量有限制建议长期活动用固定码短期裂变活动用临时码。扫码入口的海报上如果印了过期日期用户会失去参与感所以活动结束前三天要主动在后台续期。3.4 链接触发关键字给任意 URL 配一个领取口“支持设置链接触发任何关键字”的意思是模块可以为一个应用场景绑定任意关键字用户访问 H5 页面时页面上通过微信 JS-SDK 自动发送该关键字到公众号对话框从而触发 processor.php 里的回复。实现原理是公众号 H5 页里调用微信 JS-SDK 的wx.invoke(sendAppMessage)或者直接引导用户复制关键字回公众号聊天窗口。更稳定的是使用微信的“网页直接跳转聊天窗口”能力在 H5 里构造带关键字的链接。这个方案需要认证服务号和网页授权。uniapp 开发 H5 嵌入微信公众号中获取定位那一套流程本质也是同一个 JSSDK 配置区别在于要的权限不同。这里给一个关键字触发判断的写法$content trim($this-message[content]); if ($content $setting[trigger_keyword]) { $this-sendCard($this-message[from]); }参数说明trigger_keyword在后台维护可以配置多个逗号分隔。sendCard里会再次校验活动时间和粉丝条件避免用户绕过 H5 直接发关键字也能领到卡。实际运营时关键字不要用“卡密”“领取”这种容易被机器扫描的词建议用随机度高一点的活动码比如DKA7F3这样能减少恶意请求。4. 卡号密码模式、H5 表单与支付售卖的流程拆解4.1 卡号密码模式的会话状态管理支持卡号密码模式即用户先发送卡号模块再自动返回对应密码。这个交互不是一次性的而是有状态用户第一次发送卡号时模块回复“已进入卡密查询模式”如果用户下一个动作是发送卡号模块返回密码。1.1.27 版本里状态通常存在缓存或模块表里。if ($content 卡密模式) { $cache-set(ld_mode_ . $openid, cardkey, 600); $this-replyText(已进入卡密模式请发送您要查询的卡号); return; } if ($cache-get(ld_mode_ . $openid) cardkey) { $password $this-findPasswordByCard($content); if ($password) { $this-replyText(卡号 . $content . \n密码 . $password); } else { $this-replyText(未找到该卡号请检查后重试); } }这里用缓存做模式开关TTL 设为 600 秒用户十分钟不操作自动退出模式。如果不用缓存直接查库判断用户当前处于什么状态高并发下会有重复发放问题。注意回复完密码后要主动删除模式标记否则用户继续发下一条卡号会再次触发查询这个行为在防刷场景反而不利。另外模式标记的 key 一定要拼上 openid不然两个用户切换查询会互相串号这是线上出现过的事故。4.2 用户输入手机号后获取卡密的表单提交链路用户输入手机号的设计一般是在 H5 页面收集手机号然后调用模块的site.php里的doMobileVerify()方法。手机号格式校验要在前端和后端都做后端优先用正则public function doMobileVerify() { $mobile trim($_POST[mobile]); if (!preg_match(/^1[3-9]\d{9}$/, $mobile)) { json([code 0, msg 手机号格式错误]); } $card $this-lockCardForMobile($mobile); ... }校验通过后不能直接把卡密返回给前端因为前端拿到卡密就能刷接口无限领取。正确顺序是先给用户 openid 绑定手机号再从卡密池里取一张未分配的记录用事务锁住card_no再写入领取记录最后通过模板消息或公众号客服消息把卡密推到微信里。这样一个手机号只能领一次就算 H5 接口被刷也只会返回“该手机号已领取”的提示拿不到第二张卡。4.3 支付收费售卖卡密的回调闭环支付售卖是模块里最复杂的一段涉及微信支付统一下单、回调验签、卡密发放三个环节。模块配置里需要填商户号、API 密钥、证书路径微擎的支付类已经封装了统一下单但回调地址必须指向模块自己的通知地址不是微擎默认的支付回调。public function doPayNotify() { $raw file_get_contents(php://input); $data xml2array($raw); if ($data[result_code] ! SUCCESS) { return FAIL; } $orderNo $data[out_trade_no]; $order $this-getOrder($orderNo); if ($order[status] 0 $this-verifySign($data)) { $this-releaseCard($order[id]); $this-markOrderPaid($orderNo); } return SUCCESS; }回调里返回SUCCESS给微信支付服务器表示已收到通知否则微信会持续重试。这里常见的坑是按文档直接返回xml格式的SUCCESS微擎的xml2array函数对大小写敏感建议统一大写成SUCCESS。另外订单状态判断必须在验签之后防止伪造回调把未支付的订单标记成已支付。releaseCard 发放卡密后要保证幂等即订单已经发过卡就不要再发第二次靠订单状态字段控制。还有回调日志必须记录原始报文线上排查问题时微信支付重试记录和本地回调日志对照着看能省一半时间。4.4 H5 页面与 uniapp 嵌入时的定位适配如果卡密领取页是用 uniapp 开发的 H5嵌入微信公众号时会遇到定位授权的问题。定位和卡密业务没有直接关系但很多运营会要求“只有本地用户才能领”此时就要用到微信 JS-SDK 的getLocation。模块的公众号后台需要绑定 JS 接口安全域名前端wx.config的签名由模块提供签名算法涉及 timestamp、nonceStr、url。签名失败常见原因是页面 url 参数被微信自动增加了fromsinglemessage等参数导致签名校验不过。wx.ready(() { wx.getLocation({ type: wgs84, success: (res) { const lat res.latitude; const lng res.longitude; // 调用模块接口校验地理位置 } }); });注意wgs84坐标和腾讯地图的gcj02不一样如果要在地图上画围栏需要转换成 gcj02。这个转换通常由后端完成前端把原始坐标传回不要在前端集成加密库增加包体积。定位拿不到时页面要给出降级文案比如“当前无法获取位置请在微信内打开”不要直接报错让用户卡在空白页。5. 活动时间、粉丝性别与新老粉丝限制的后台配置5.1 活动开始与结束时间的边界处理后台控制活动开始和结束时间看似简单实际边界很多。活动开始时间建议精确到分钟结束时间到秒。因为微擎的time()取的是服务器时间和用户本地时间有差异服务器时区建议设置为 PRC。if ($now $setting[start_time]) { $this-debugLog(未开始, $openid); return $this-replyText(活动尚未开始); } if ($now $setting[end_time]) { $this-debugLog(已结束, $openid); return $this-replyText(活动已经结束); }end_time用而不是结束时间点那秒就禁止领取。如果活动是永久的时间字段留空或者设为 0判断逻辑要加上empty($setting[end_time])跳过结束判断否则 1970 年的时间戳会直接导致活动在开通瞬间就过期这是 PHP 开发者常犯的坑。还有夏令时问题国内没有夏令时但如果服务器租在海外建议取北京时间字符串再转时间戳不要依赖系统默认时区。5.2 粉丝性别限制的实现与盲区性别限制字段后台一般有三个选项不限、男、女。性别数据从哪里来微信用户信息接口已经逐渐不返回性别新注册的公众号应用可能一直拿到的是“未知”。1.1.27 的性别判断逻辑是先从本地mc_members表拿如果表里没有模块不会实时去拉取微信接口因为接口权限收紧后拉不到会用“未知”处理。如果活动必需要求男或女建议在 H5 表单里让用户自选性别再写入模块扩展表。公众号对话场景下性别判断只能做降级未知性别的用户默认放行还是拦截取决于运营要的是拉新还是精准筛选。如果活动奖品是女性向产品未知性别建议放行因为男用户参与后也不会产生价值最多领走一张卡密而拦截未知性别会把已经订阅的老用户挡在门外反而影响活动传播。5.3 新老粉丝限制的筛选逻辑新老粉丝限制有三个档位全部、仅新粉丝、仅老粉丝。实现的核心是判断用户是否曾经关注过微信接口没有直接暴露“粉丝首次关注时间”所以模块要自己记录关注事件。receiver.php 的 subscribe 分支里每次事件到达都会查询ld_user_subscribe_log表如果不存在记录就视为新粉丝并写入当前时间。$subLog pdo_get(ld_subscribe_log, [openid $openid]); if (empty($subLog)) { pdo_insert(ld_subscribe_log, [openid $openid, subscribe_time time()]); $isNewFan true; } else { $isNewFan false; }注意这个逻辑的前提是用户从未关注过模块里没有任何历史记录。如果用户曾经取关再关注subscribe 事件再次触发但表里已有记录会被判定为老粉丝。这符合大多数活动“只允许新用户参与”的需求。熟悉数据的人可能会想用取关日志清掉记录但清掉后老粉取关再关注又变成新粉会低成本绕过限制。更好的方案是保留首次关注时间但用unsubscribe_time和subscribe_time两个字段判断只有用户取关超过三十天才算重新拉新这个策略在 1.1.27 里没有需要二次开发。考虑到模块的定位是快速落地默认实现已经够用超过三十天的回流用户本身就有二次触达价值。6. 后台批量删除、领取记录清理与上线自检6.1 批量删除卡密与领取记录的 SQL 边界后台支持批量删除卡密及领取记录。微擎后台列表页一般会提供多选框删除操作走的是 foreach 循环逐条 delete。数据量大时逐条删除会触发多次 SQL 查询卡密表几万条数据时后台操作会卡几十秒。更合理的做法是直接构造 in 条件一次性删除DELETE FROM ims_ld_card WHERE id IN (1201, 1202, 1203);注意微擎的数据表前缀是ims_具体前缀取决于安装时配置。批量删除卡密前一定要先删领取记录表里的关联数据否则后台列表里出现“已领取但卡密不存在”的脏数据。如果不想物理删除建议加一个status -1的软删除字段后台默认查询只过滤status 0这样既能回收卡密池也能保留审计记录。6.2 删除后库存同步的幂等处理删除卡密后活动剩余库存要重新统计。因为删除操作和库存统计是两个独立动作中间可能出现并发。上线初期建议把库存统计写成实时 count而不是在卡密表里维护一个库存数字段。原因是批量导入卡密、单张删除、活动自动发卡都会改库存任何一个入口忘记更新数字段都会出现“库存显示还有 10 张实际一张也发不出来”的情况。6.3 上线自检的三条命令上线前用三条命令快速检查模块状态php -l application/modules/ld_sendcode/site.php php -l application/modules/ld_sendcode/receiver.php mysql -e select count(*) from ims_ld_card where status0;php -l是语法检查确保上传源码时没有因为 FTP 传输模式导致文件损坏。第三句是统计库存如果 status0 的数量是 0但后台活动还开着说明卡密池已空需要先导入数据再开放活动。检查完语法和库存后再拿一个小号实测关注、取关、再关注三种触发方式重点看 receiver.php 的 debug log 里有没有重复发卡的记录。这个版本没有改业务逻辑只补了一个细节所有领取操作都统一走 ld_send_card() 方法方法内部同一个 openid 在 60 秒内只允许发放一次。第二笔领取会返回提示“操作过于频繁”就是这一行判断能挡掉绝大多数手动刷卡密的行为也顺手解决了菜单和扫码同时触发时的重复发放问题。本文还有配套的精品资源点击获取