在微信公众号运营场景里,积分商城是最常见的活跃手段之一。用户点击底部菜单里的「积分商城」,页面打开后直接显示自己的当前积分和可兑换商品,这种体验背后的实现涉及三件事:自定义菜单的创建、菜单事件的处理、以及通过网页授权识别用户身份后查询积分。这篇文章把整条链路拆开来讲,并附上可以直接参考的代码。

一、自定义菜单的两种类型:click与view的选择
公众号自定义菜单的按钮类型里,和积分商城相关的主要是两种:click和view。click类型的按钮被点击后,微信会以事件消息的形式推送到开发者配置的服务器地址,你的服务端收到XML或JSON报文后可以自由决定做什么,比如回复一条图文消息、下发一个跳转链接。view类型的按钮则直接跳转到你指定的URL,不经过事件推送。
对于积分商城这种H5页面,推荐用view类型,URL指向一个带网页授权的中间页。如果你的业务还需要在点击时记录行为、做埋点或下发卡券,可以再加一个click按钮先接收事件再引导跳转。两种方式各有取舍:view实现简单、跳转快;click灵活但多一次服务端交互。
创建菜单需要调用微信的接口,地址是https://api.weixin.qq.com/cgi-bin/menu/create?access_token=ACCESS_TOKEN。请求体示例:
{
"button": [
{
"type": "view",
"name": "积分商城",
"url": "https://yourdomain.com/mall/auth"
},
{
"type": "click",
"name": "我的积分",
"key": "MY_POINTS"
}
]
}这里的url建议不要直接写商城最终页面,而是写一个授权中转地址。因为要在商城里显示用户积分,就必须知道用户是谁,这就涉及下一节的网页授权。
二、网页授权获取用户身份:openid是积分查询的钥匙
识别用户身份靠的是微信的OAuth2.0网页授权机制。流程是:用户点击菜单跳转到你的中转页,中转页重定向到微信授权地址,微信带上code回调你的redirect_uri,服务端用code换取用户信息。如果公众号是已认证的服务号,使用snsapi_base静默授权即可拿到openid,用户全程无感知,体验最好。
授权地址的格式为:https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect。注意redirect_uri需要urlencode处理,state可以带上回跳目标参数。服务端用code换取openid的代码示例如下:
<?php
// 中转页:引导用户进入网页授权
$appid = '你的APPID';
$redirect = urlencode('https://yourdomain.com/mall/callback');
header('Location: https://open.weixin.qq.com/connect/oauth2/authorize?' .
'appid=' . $appid .
'&redirect_uri=' . $redirect .
'&response_type=code' .
'&scope=snsapi_base' .
'&state=mall' .
'#wechat_redirect');
exit;
// callback.php:用code换取openid
function getOpenid($appid, $secret, $code) {
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?" .
"appid={$appid}&secret={$secret}&code={$code}&grant_type=authorization_code";
$res = json_decode(file_get_contents($url), true);
return $res['openid'] ?? null;
}有一个容易被忽略的坑:code只能使用一次,且有效期为5分钟。用户频繁刷新回调页时会报40163错误(code been used),解决办法是在回调成功后把openid写入session,后续请求直接走session,不再重复授权。另外redirect_uri的域名必须与公众号后台「网页授权域名」配置一致,且需要放置MP_verify开头的校验文件到服务器根目录。
三、服务端查询积分并渲染商城页面
拿到openid后,积分查询就是纯业务逻辑了。你的用户体系里应该有一张绑定表,把openid和系统内的user_id关联起来。查询积分时按openid找到user_id,再从积分表汇总。下面是一个Node.js加Express的完整示例:
const express = require('express');
const axios = require('axios');
const app = express();
app.get('/mall/callback', async (req, res) => {
const { code } = req.query;
// 用code换取openid
const tokenRes = await axios.get('https://api.weixin.qq.com/sns/oauth2/access_token', {
params: {
appid: process.env.WX_APPID,
secret: process.env.WX_SECRET,
code,
grant_type: 'authorization_code'
}
});
const openid = tokenRes.data.openid;
// 根据openid查询用户积分
const user = await db.query('SELECT user_id FROM user_bind WHERE openid = ?', [openid]);
if (!user.length) {
return res.render('mall', { openid, points: 0, isBound: false });
}
const points = await db.query(
'SELECT COALESCE(SUM(change_value), 0) AS total FROM points_log WHERE user_id = ?',
[user[0].user_id]
);
res.render('mall', { openid, points: points[0].total, isBound: true });
});积分商城页面在渲染时,建议把积分值直接输出到HTML里而不是靠前端异步请求,这样首屏就能展示积分,用户感知更快。如果商城是前后端分离的单页应用,则可以在前端先展示骨架屏,再用openid对应的token去调积分接口。注意接口层一定要做鉴权,不能只凭前端传来的openid就返回积分——openid虽然是用户标识,但它不是秘密,被伪造后任何人都能查别人的积分。正确做法是授权成功后签发一个短有效期的JWT,前端携带token调接口,服务端验证签名后再查询。
四、click事件推送的处理与常见问题
如果你选择用click类型按钮做「我的积分」这类快捷查询,就需要处理微信推送的事件消息。微信会把XML报文POST到你配置的服务器URL,其中Event字段为CLICK,EventKey是你创建菜单时设置的key。服务端解析出FromUserName(即用户openid)后查积分,再以被动回复消息的形式返回。Node.js里的处理示例:
app.post('/wx/message', express.text({ type: 'text/xml' }), (req, res) => {
parseXML(req.body).then(async (msg) => {
if (msg.Event === 'CLICK' && msg.EventKey === 'MY_POINTS') {
const openid = msg.FromUserName;
const points = await getPointsByOpenid(openid);
// 被动回复文本消息
res.send(buildTextReply(msg.ToUserName, openid,
'您当前积分:' + points + ',点击菜单「积分商城」可去兑换好礼'));
} else {
res.send('success');
}
});
});最后梳理几个高频报错。菜单创建返回40019说明button的key冲突;返回48001说明接口未授权,自定义菜单接口只有认证后的公众号才能用,个人订阅号不支持。获取access_token时出现40125是AppSecret错误,建议把access_token集中存到Redis并设置7000秒左右的过期时间,避免频繁刷新触发限流。token与普通access_token是两个不同的东西,别混用。整套链路跑通后,从用户点击菜单到看到自己的积分,整个体验可以控制在一秒以内。
微信公众号自定义菜单积分商城网页授权修改时间:2026-09-10 04:24:33