
简介这是一份面向公众号运营者和开发者的关注送卡密源码包版本1.1.27用于快速搭建公众号自动发卡、粉丝福利与付费变现场景。源码实现了关注自动发送、菜单点击领取、认证服务号扫码领取等多种触发方式同时内置卡号密码模式——用户发送卡号即可自动获取对应密码后台支持批量删除卡密与领取记录、设置活动起止时间、限定粉丝性别与新老粉丝、绑定链接触发关键字还能开启支付收费售卖卡密及手机号获取流程几乎覆盖当前公众号点卡营销的主流玩法。压缩包共27个文件以13个PHP业务文件为核心配合4个HTML页面、2个JS脚本、2个Excel模板和若干图片素材整体仅199KB结构紧凑便于直接部署或二次修改。目前已有567人学习下载适合需要快速上线公众号自动发卡活动、希望研究卡密发放与领取逻辑的中初级开发者参考。1. 关注送卡密系统的核心逻辑与技术选型所谓“公众号源码 关注送卡密”本质是微信生态里一套「用户身份核验 数字商品自动交付」的闭环用户关注公众号后台拿到用户的 openid在卡密池里取一个未使用的激活码通过自动回复推给用户同时在数据库里记下 openid 与卡密的唯一绑定关系。这套逻辑看起来简单但实际落地时涉及公众号类型选型、事件回调签名验证、卡密库存管理、重复关注防刷、被动回复超时等一系列问题任何一环出毛病都会出现“用户关注了没反应”或“一个卡密发给两个人”这类事故。这个标题里的 1.1.27 是源码的迭代版本号。版本迭代通常意味着两点一是关注事件的兼容性处理越来越稳比如服务号、订阅号、测试号的差异被拉平二是卡密发放策略从“一次性拉取全部”改为“按库存动态分配”并加上了绑定表来兜底。本文不依赖任何特定版本而是把这些系统都会涉及的代码逻辑、表结构、参数和排错经验整理成一套可直接复用的方案。适合三类人看运营付费资源号的个人站长、接私活做公众号裂变工具的独立开发者、以及需要在企业公众号里做“关注发券”场景的后端工程师。2. 搭建环境公众号配置、服务器代码与卡密表设计关注送卡密的完整链路从微信公众号平台的后台配置开始到数据库里多出一行卡密记录结束。先确保服务器上有可用的 Web 服务能跑 PHP 或 Python能连 MySQL 或 Redis。本文代码以 PHP 为例因为这是 PHP 源码生态里最常见的组合宝塔面板部署成本低排错资料也多。2.1 公众号类型与接口权限边界订阅号有服务器配置入口能接收关注/取关事件但被动回复的消息里不能带外链跳转菜单网页授权仅支持静默获取 openid。服务号支持网页授权 snsapi_userinfo可以拿用户头像昵称还支持客服消息主动推送。发卡后用客服消息补发服务号显然更稳。测试号接口权限接近服务号适合本地开发联调但正式上线必须换成认证后的服务号或订阅号。在后台「设置与开发 - 基本配置」里把服务器 URL 填成https://你的域名/wx/callback.phpToken 随便填一串随机字符串EncodingAESKey 点随机生成。消息加解密方式建议先选“明文模式”跑通再切“安全模式”不要一上来就开加密否则排错时百度和日志都救不了你。2.2 后端语言与部署方式选择PHP 方案不需要常驻进程nginx php-fpm 就能跑事件回调天然是 HTTP 请求代码崩溃不影响微信服务器。配合 MySQL 存卡密Redis 做发放锁是性价比最高的组合。Python 方案用 Flask 或 FastAPI 写回调适合团队本来就以 Python 为主的情况但生产环境要注意 gunicorn 的 worker 数量避免微信服务器重试时多进程重复发放。Node 方案适合顺手就用 Koa/Express 的场景但要注意微信回调的 5 秒超时Node 的异步模型反而更容易在这里踩坑因为 async 操作没 await 完就直接 return 了。我一般直接用 PHP MySQL卡密系统属于典型 IO 密集型语言本身没有性能瓶颈真正重要的是发放动作的原子性也就是不能让两个并发的关注事件取到同一个卡密。2.3 数据库表结构与发放状态机卡密表和领取记录表分开建不要混在一张表里。卡密表只关心“这个卡密是否可用、被谁在什么时候绑定”领取记录表只关心“这个 openid 在什么时间领了哪个卡密”。CREATE TABLE card_keys ( id int(11) NOT NULL AUTO_INCREMENT, card_key varchar(64) NOT NULL COMMENT 卡密内容, batch_no varchar(32) DEFAULT COMMENT 批次号用于区分活动, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0未发放 1已发放 2已兑换, openid varchar(64) DEFAULT COMMENT 绑定的用户openid, expire_time datetime DEFAULT NULL COMMENT 卡密过期时间null为永久, created_at datetime DEFAULT CURRENT_TIMESTAMP, bind_time datetime DEFAULT NULL COMMENT 发放时间, PRIMARY KEY (id), UNIQUE KEY uk_card_key (card_key), KEY idx_status (status), KEY idx_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user_claims ( id int(11) NOT NULL AUTO_INCREMENT, openid varchar(64) NOT NULL, card_key varchar(64) NOT NULL, scene varchar(16) DEFAULT subscribe COMMENT 领取场景subscribe关注/scan扫码/reply回复, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid_card (openid, card_key), KEY idx_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段和索引是这套系统里最值得花时间设计的地方宁可建表时多写两个索引也不要等卡密池涨到十万条再来加索引。uk_card_key唯一索引是发货的底线保障数据库层面直接拒绝重复的卡密内容idx_openid则用来快速判断一个用户是否已经领过。status字段从 0 到 1 再到 2表示未发放、已发放给用户、已被用户兑换状态机从数据库层面保证不会出现“已发放但没绑定 openid”的脏数据。2.4 微信服务器配置与 token 验签的坑后台填写的 Token 不是用来鉴权的而是用来算签名做校验的。微信服务器每次请求都会带上signature、timestamp、nonce三个参数处理逻辑是把 Token、timestamp、nonce 三个参数字典序排序拼接成字符串后做 sha1 加密再和 signature 比对。// wx_callback.php 入口文件 ?php define(WX_TOKEN, your_random_token_here); $timestamp $_GET[timestamp]; $nonce $_GET[nonce]; $signature $_GET[signature]; $tmpArr [WX_TOKEN, $timestamp, $nonce]; sort($tmpArr, SORT_STRING); $tmpStr implode($tmpArr); $tmpStr sha1($tmpStr); if ($tmpStr ! $signature) { exit(signature error); } // GET 请求是 URL 验证回显 echostr if ($_SERVER[REQUEST_METHOD] GET) { echo $_GET[echostr]; exit; } // POST 请求是事件或消息推送走业务处理 $xmlData file_get_contents(php://input); // 解析并处理...验签失败最常见的三个原因一是 Token 里带了空格或不可见字符二是服务器时间与标准时间偏差过大导致 timestamp 被判定为过期三是把 GET 验证和 POST 接收放在了同一个文件里但没有按请求方法区分导致验签通过后直接打印了 XML 而不是 echostr。解法是严格按方法分流并且在日志里记录 signature、timestamp、nonce 和原始 XML方便对照微信官方文档排查。3. 实现关注自动发卡事件回调、卡密池与回复模板配置好入口和数据库表之后本逻辑分三步解析微信推送的 XML、识别关注事件、领取卡密并组织回复内容。整个流程的核心是“事件驱动”不要用定时任务去扫表微信推一次事件你就发一次卡这样既不浪费接口配额也能保证实时性。3.1 解析 XML 并识别 subscribe 事件微信推过来的消息是 XML 格式MsgType为event时Event字段值为subscribe就是新用户关注unsubscribe是取关SCAN是已关注用户扫码。注意区分用户未关注时扫码进入公众号推的是subscribe事件且带EventKey用户已关注时扫码推的是SCAN事件。两种场景可以分别处理比如未关注用户扫码后除了发卡还要引导他绑定手机号已关注用户扫码则只回复当前卡密状态。// 解析微信 XML $xml simplexml_load_string($xmlData, SimpleXMLElement, LIBXML_NOCDATA); $fromUser (string)$xml-FromUserName; // 用户的 openid $toUser (string)$xml-ToUserName; // 公众号原始 ID $msgType (string)$xml-MsgType; $event (string)$xml-Event; if ($msgType event $event subscribe) { // 先从绑定表查是否已领过防止重复发放 $claim db_query(SELECT card_key FROM user_claims WHERE openid {$fromUser} LIMIT 1); if ($claim) { reply_text($fromUser, $toUser, 您已领取过卡密 . $claim[card_key]); exit; } // 领取卡密并绑定 openid $cardKey issue_card($fromUser); if ($cardKey) { reply_text($fromUser, $toUser, 感谢关注您的卡密是{$cardKey}\n有效期至2025-12-31); } else { reply_text($fromUser, $toUser, 卡密库存不足请联系管理员补货); } }这里的关键是查一次再发一次而不是直接 update 卡密表。user_claims表在前面建表时已经把openid和card_key都加了唯一索引即使两个并发请求同时进来数据库的唯一约束也会拒绝第二次插入业务代码拿不到重复发放的写入结果就自然走不下去。3.2 卡密池发放策略顺序发放与随机发放卡密发放有两种常见策略。顺序发放是按id升序取最小未发放卡密好处是卡密使用率发放率便于按批次消耗坏处是长时间不活跃的老卡密会堆积在前段。随机发放是从全部可用记录里随机取一条能打散不同批次的卡密让每个批次都被均匀消耗但 SQL 写法上要避免大表的ORDER BY RAND()否则十万条卡密时一条查询就能把数据库 CPU 打满。function issue_card($openid) { // 开启事务确保取卡和绑定是原子操作 $pdo-beginTransaction(); try { // 随机发放但避免 ORDER BY RAND() $stmt $pdo-query(SELECT id, card_key FROM card_keys WHERE status 0 AND (expire_time IS NULL OR expire_time NOW()) ORDER BY id ASC LIMIT 1 FOR UPDATE); $row $stmt-fetch(PDO::FETCH_ASSOC); if (!$row) { $pdo-rollBack(); return false; } $upd $pdo-prepare(UPDATE card_keys SET status 1, openid ?, bind_time NOW() WHERE id ? AND status 0); $upd-execute([$openid, $row[id]]); $pdo-commit(); // 写入领取记录 $pdo-prepare(INSERT INTO user_claims (openid, card_key) VALUES (?, ?)) -execute([$openid, $row[card_key]]); return $row[card_key]; } catch (Exception $e) { $pdo-rollBack(); log_error(issue_card failed: . $e-getMessage()); return false; } }FOR UPDATE是真正防并发超发的那把锁。同一时刻两个用户同时关注MySQL 的行锁会让第二个请求在SELECT处等待第一个事务提交后第二个拿到的已经是status1的记录UPDATE ... WHERE status0影响 0 行事务回滚他就只能收到“库存不足”的提示。LIMIT 1 FOR UPDATE组合比单纯靠唯一索引更可靠性能上对十万级卡密表也没有压力。3.3 被动回复的 XML 模板与消息长度限制微信被动回复有严格的 5 秒超时限制超时后微信会判定回复失败用户侧什么都看不到而服务器日志里可能根本没有记录。回复内容需要按微信规定的 XML 模板拼装文本消息最简单图片、图文消息需要额外准备素材。xml ToUserName![CDATA[oXXXX-openid]]/ToUserName FromUserName![CDATA[gh_xxxx]]/FromUserName CreateTime1735689600/CreateTime MsgType![CDATA[text]]/MsgType Content![CDATA[您的卡密ABC-123-DEF请勿泄露给他人。]]/Content /xml被动回复的 XML 里ToUserName是用户的 openidFromUserName是公众号的原始 ID两个值都是从微信推送的原始 XML 里解析出来的不要自己填充否则会报invalid FromUserName。文本内容的长度建议控制在 600 字以内实测超过 2048 字节部分版本的微信客户端会截断显示而且用户复制卡密时也容易出错。3.4 客服消息补发与异常兜底被动回复一旦超时或失败微信不会自动重推事件所以需要在业务里加一道兜底把发放结果写好日志同时把“用户-卡密”的绑定关系落库。等用户下次主动发消息时在消息处理器里检查user_claims表没领到卡密的用户自动补发。// 用户主动发消息时检查是否已发过卡 if ($msgType text) { $claim db_query(SELECT card_key FROM user_claims WHERE openid {$fromUser} LIMIT 1); if (!$claim) { $cardKey issue_card($fromUser); reply_text($fromUser, $toUser, $cardKey ? 补发卡密{$cardKey} : 库存不足请稍后再试); } else { reply_text($fromUser, $toUser, 您已领取过卡密为 . $claim[card_key]); } }这个兜底逻辑顺便解决了一个真实痛点用户关注后马上退出没有等到被动回复弹出或者微信后台明明显示发送成功但用户没看见。把“发卡”和“回复”解耦发卡是数据库行为回复是展示行为即使回复丢失数据库里的绑定关系还在下次对话随时可以补。4. 防刷、超时与误发参数调优和排错备案关注送卡密系统上线后运维负担集中在三类问题上恶意刷卡、被动回复超时、卡密库存管理失控。这一章把这些问题对应的防御手段和参数整理成表。4.1 重复关注、取关再关注的防刷策略微信的subscribe事件只在用户从未关注到关注时推一次取关后再关注会再推一次。单纯靠user_claims唯一索引能拦截同一 openid 重复领卡但拦截不了“同一微信号取关再关注”的循环刷账号本身还是一个 openid取关后重新关注绑定表里已有记录不会发新卡。真正要防的是“同一个人用多个微信号反复关注”这类刷法没有完美解只能加限制条件。策略实现方式效果误伤率openid 唯一绑定user_claims 唯一索引拦截同号重复领无IP 频率限制记录关注事件的来源 IP每分钟 10 次告警拦截批量脚本低同设备限制通过 JS-SDK 获取设备指纹需网页授权拦截模拟器高分享助力任务邀请 N 人关注解锁下一张卡密提高刷卡成本中IP 限制要注意代理出口是同一个 IP 的情况比如公司 Wi-Fi 下多个同事同时关注容易出现误杀所以阈值要放宽到每分钟 30 次以上。设备指纹的方案需要在用户关注后跳转一个 H5 页面用uniapp 开发 H5 嵌入微信公众号中获取定位这类能力做二次核验会增加用户操作步骤适合高价值卡密场景。4.2 被动回复超时异步任务与消息队列微信的 5 秒超时对 PHP 来说相当紧张尤其是发放卡密需要查库、写库、再拼 XML。如果数据库查询超过 100ms在网络抖动时很容易整条链路超时。常见的解法是把发放动作前置收到subscribe事件后先立即回复“正在生成卡密请稍候”然后通过 Redis 队列把发放任务异步化等发放完成后用客服消息接口主动推送给用户。但要注意客服消息接口只允许在用户 48 小时内有互动时发送所以不能单纯依赖客服消息异步方案必须配合用户主动发消息时补发的兜底。// 异步队列消费端示例 $redis new Redis(); $redis-connect(127.0.0.1, 6379); while ($job $redis-brpop(card_issue_queue, 5)) { $data json_decode($job[1], true); $openid $data[openid]; // 检查是否已解决防止重复消费 $exists db_query(SELECT id FROM user_claims WHERE openid {$openid} LIMIT 1); if ($exists) { continue; } $cardKey issue_card($openid); if ($cardKey) { send_custom_text($openid, 您的卡密已生成{$cardKey}); } }队列消费要注意两点一是消费端必须做幂等同一个 openid 的发放任务可能被投递多次消费前先查user_claims表二是队列消费失败要有重试机制建议消费端捕获异常后把任务重新塞回队列并限制重试次数为 3超过 3 次进入死信队列人工处理。4.3 库存水位告警与批次过期参数卡密池的库存管理比想象中重要。设计一个简单的库存监控当可用卡密数量低于阈值时给管理员推送一条模板消息。// 库存检查建议每分钟跑一次 $count db_query(SELECT COUNT(*) FROM card_keys WHERE status 0 AND (expire_time IS NULL OR expire_time NOW())); $threshold 100; // 告警阈值 if ($count $threshold) { send_template_message_to_admin(卡密库存不足, 当前剩余可用卡密{$count}请及时补充); }阈值要根据日关注量动态调整比如日均新增关注 500 人阈值至少要设为 1500留出 3 天安全余量。此外卡密表里的expire_time字段控制的是卡密本身的失效时间不是发放时间。如果运营活动只持续一个月发放时就把失效时间写死用户领取后过期无法兑换这个时间需要在回复文案里写清楚避免用户领到一张“看起来能用但实际已失效”的卡密引发投诉。5. 验证整套流程与上线后的三个实用技巧系统上线前用模拟请求把整个链路过一遍比在后台点“发送”按钮高效得多。这一章分享三个在实际部署中经常能派上用场的技巧。5.1 用 curl 本地模拟微信服务器回调把wx_callback.php部署到服务器后不要急着用手机扫码关注先用 curl 伪造微信服务器的验证请求和事件推送。# 计算签名把 Token、timestamp、nonce 按字典序拼接后 sha1 # 假设 Tokenabc123, timestamp1735689600, nonce123456 # 排序后拼接123456abc1231735689600 SIGNATURE$(echo -n 123456abc1231735689600 | openssl sha1 | awk {print $2}) # URL 验证 curl https://yourdomain.com/wx/callback.php?signature${SIGNATURE}timestamp1735689600nonce123456echostrhello_wx # POST 模拟关注事件需要把 XML 里的 openid 改成你自己的测试号 openid curl -X POST https://yourdomain.com/wx/callback.php \ -H Content-Type: text/xml \ -d xmlToUserName![CDATA[gh_test]]/ToUserNameFromUserName![CDATA[o_test_openid]]/FromUserNameCreateTime1735689600/CreateTimeMsgType![CDATA[event]]/MsgTypeEvent![CDATA[subscribe]]/Event/xml验证时看两点URL 验证返回的字符串是不是hello_wxPOST 事件推送后数据库里是否多了一条user_claims记录且重复执行第二次时回复内容应该是“您已领取过卡密”而不是再发一张新卡。5.2 把日志做成“追踪链路”而不是“错误堆栈”排错时最怕的是日志只在 catch 里打一行“系统错误”。建议从一开始就给每次事件生成一个request_id用 openid 加微秒时间戳组合然后把验签参数、原始 XML、数据库发放结果、回复 XML 全部记录在同一个日志条目里。用 PHP 可以直接写文件或者交给 monolog 这类库但重点不是用什么库而是日志里必须包含四个信息事件类型、openid、卡密值、最终回复状态。没有 openid 的日志等于白记因为微信后台的报错提示远不如你日志里的 openid 定位来得快。5.3 卡密批次前缀设计与批量导入卡密表里的batch_no字段是运营后期的大杀器。上线时批量导入卡密建议格式为{批次前缀}-{随机序列}比如V001-KD8F-23A9批次前缀既能快速定位某个活动发出去的卡密来自哪个采购批次也方便后期做数据统计查哪个批次兑换率高、哪个批次剩余多。批量导入时不需要逐条 insert准备好 CSV 后用LOAD DATA LOCAL INFILE批量写入十万条卡密秒级入库然后再统一用一条 UPDATE 语句把已过期的卡密批量标记为不可用避免业务代码里每次取卡都带一个expire_time NOW()的条件给 SELECT 加不必要的负担。最后留一个上线后最容易踩的细节客服消息接口对 48 小时互动窗口的限制会让“补发卡密”和“续期提醒”这类主动推送经常失败报错码是 45015。所以对已发卡的用户做任何主动触达都要引导对方先点一次公众号菜单或发一条消息把 48 小时互动窗口重新打开再执行客服消息发送。关注送卡密的整个链路里这 48 小时窗口就是你跟用户建立二次触达的黄金时间错过这个窗口用户就真的只留下一张卡密记录连补发都无门了。本文还有配套的精品资源点击获取