微信公众号 - 实际例子
通过一个完整的"智能客服公众号"例子,展示公众号开发的完整流程:服务器搭建、消息处理、菜单创建、网页授权。
项目结构
├── server
│ ├── index.js // 入口文件(Express服务)
│ ├── wechat.js // 微信消息处理
│ ├── menu.js // 自定义菜单管理
│ ├── auth.js // OAuth网页授权
│ └── token.js // access_token管理
├── public
│ └── user.html // 用户中心网页
├── package.json
└── .env // 配置文件(AppID等)
server/index.js - 入口文件
const express = require('express')
const crypto = require('crypto')
const { handleMessage } = require('./wechat')
const { handleAuth } = require('./auth')
const getRawBody = require('raw-body')
const app = express()
const TOKEN = process.env.WX_TOKEN
app.get('/wechat', (req, res) => {
const { signature, timestamp, nonce, echostr } = req.query
const hash = crypto.createHash('sha1')
.update([TOKEN, timestamp, nonce].sort().join(''))
.digest('hex')
res.send(hash === signature ? echostr : 'failed')
})
app.post('/wechat', async (req, res) => {
const xml = await getRawBody(req, { encoding: 'utf-8' })
const reply = await handleMessage(xml)
res.type('application/xml').send(reply)
})
app.get('/auth', handleAuth)
app.use(express.static('public'))
app.listen(3000, () => console.log('服务启动于端口 3000'))
server/wechat.js - 消息处理
const xml2js = require('xml2js')
const KEYWORDS = {
'帮助': '可用命令:\n1. 天气+城市名 查询天气\n2. 签到 每日签到\n3. 帮助 查看说明',
'签到': '签到成功!今日积分+10'
}
function buildReply(to, from, content) {
return `<xml>
<ToUserName><![CDATA[${to}]]></ToUserName>
<FromUserName><![CDATA[${from}]]></FromUserName>
<CreateTime>${Math.floor(Date.now() / 1000)}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[${content}]]></Content>
</xml>`
}
async function handleMessage(xmlData) {
const { xml } = await xml2js.parseStringPromise(xmlData)
const from = xml.FromUserName[0]
const to = xml.ToUserName[0]
const type = xml.MsgType[0]
if (type === 'event') {
const event = xml.Event[0]
if (event === 'subscribe') {
return buildReply(from, to, '欢迎关注智能客服!\n回复"帮助"查看功能列表')
}
if (event === 'CLICK' && xml.EventKey[0] === 'SIGN_IN') {
return buildReply(from, to, '签到成功!积分+10')
}
}
if (type === 'text') {
const content = xml.Content[0].trim()
if (KEYWORDS[content]) {
return buildReply(from, to, KEYWORDS[content])
}
if (content.startsWith('天气')) {
const city = content.replace('天气', '').trim()
return buildReply(from, to, `${city}今日:晴 25°C ~ 32°C`)
}
return buildReply(from, to, '未识别的指令,回复"帮助"查看功能列表')
}
return buildReply(from, to, '暂不支持该消息类型')
}
module.exports = { handleMessage }
server/auth.js - 网页授权
const axios = require('axios')
const APP_ID = process.env.WX_APPID
const APP_SECRET = process.env.WX_SECRET
async function handleAuth(req, res) {
const { code } = req.query
if (!code) {
const redirectUri = encodeURIComponent('https://example.com/auth')
const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${APP_ID}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_userinfo&state=1#wechat_redirect`
return res.redirect(url)
}
const tokenUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${APP_ID}&secret=${APP_SECRET}&code=${code}&grant_type=authorization_code`
const { data: tokenData } = await axios.get(tokenUrl)
const userUrl = `https://api.weixin.qq.com/sns/userinfo?access_token=${tokenData.access_token}&openid=${tokenData.openid}&lang=zh_CN`
const { data: userInfo } = await axios.get(userUrl)
res.redirect(`/user.html?nickname=${encodeURIComponent(userInfo.nickname)}&avatar=${encodeURIComponent(userInfo.headimgurl)}`)
}
module.exports = { handleAuth }
public/user.html - 用户中心页面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户中心</title>
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
</head>
<body>
<div id="app">
<img id="avatar" style="width:80px;border-radius:50%">
<h2 id="nickname"></h2>
<button onclick="shareToFriend()">分享给朋友</button>
<button onclick="scanCode()">扫码签到</button>
</div>
<script>
const params = new URLSearchParams(location.search)
document.getElementById('avatar').src = params.get('avatar')
document.getElementById('nickname').textContent = params.get('nickname')
function scanCode() {
wx.scanQRCode({
needResult: 1,
scanType: ['qrCode', 'barCode'],
success(res) { alert('扫码结果:' + res.resultStr) }
})
}
</script>
</body>
</html>