微信服务号 - 实际例子
通过一个完整的"在线商城服务号"例子,展示服务号开发的核心场景:用户授权、商品下单、微信支付、模板消息通知。
项目结构
├── server
│ ├── app.js // Express入口
│ ├── routes
│ │ ├── wechat.js // 微信消息/事件处理
│ │ ├── auth.js // OAuth网页授权
│ │ ├── order.js // 订单管理
│ │ └── pay.js // 微信支付
│ ├── services
│ │ ├── token.js // access_token管理(Redis缓存)
│ │ ├── template.js // 模板消息
│ │ └── user.js // 用户管理
│ └── utils
│ └── wechat.js // 签名、XML解析等工具
├── public
│ ├── index.html // 商城首页
│ ├── order.html // 订单详情页
│ └── pay-success.html // 支付成功页
├── package.json
└── .env
server/app.js - 入口文件
const express = require('express')
const session = require('express-session')
const wechatRouter = require('./routes/wechat')
const authRouter = require('./routes/auth')
const orderRouter = require('./routes/order')
const payRouter = require('./routes/pay')
const app = express()
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}))
app.use(express.json())
app.use(express.static('public'))
app.use('/wechat', wechatRouter)
app.use('/auth', authRouter)
app.use('/order', orderRouter)
app.use('/pay', payRouter)
app.listen(3000, () => console.log('商城服务启动于端口 3000'))
server/routes/order.js - 订单与支付
const router = require('express').Router()
const crypto = require('crypto')
const { unifiedOrder, generatePayParams } = require('../utils/wechat')
const { sendTemplateMsg } = require('../services/template')
const orders = new Map()
router.post('/create', async (req, res) => {
const { openid } = req.session.user
const { productName, price } = req.body
const orderId = 'ORD' + Date.now()
const order = {
id: orderId,
openid: openid,
productName: productName,
price: price,
status: 'pending',
createTime: new Date().toISOString()
}
orders.set(orderId, order)
const prepayResult = await unifiedOrder(
openid, orderId, price * 100, productName
)
const payParams = generatePayParams(prepayResult.prepay_id)
res.json({ orderId: orderId, payParams: payParams })
})
router.get('/detail/:id', (req, res) => {
const order = orders.get(req.params.id)
if (!order) return res.status(404).json({ error: '订单不存在' })
res.json(order)
})
module.exports = router
server/routes/pay.js - 支付回调
const router = require('express').Router()
const xml2js = require('xml2js')
const getRawBody = require('raw-body')
const { verifySign } = require('../utils/wechat')
const { sendTemplateMsg } = require('../services/template')
router.post('/notify', async (req, res) => {
const xmlData = await getRawBody(req, { encoding: 'utf-8' })
const { xml } = await xml2js.parseStringPromise(xmlData)
const params = {}
Object.keys(xml).forEach(k => { params[k] = xml[k][0] })
if (!verifySign(params)) {
return res.send('<xml><return_code>FAIL</return_code></xml>')
}
if (params.result_code === 'SUCCESS') {
const orderId = params.out_trade_no
const order = orders.get(orderId)
if (order) {
order.status = 'paid'
order.transactionId = params.transaction_id
await sendTemplateMsg(order.openid, process.env.TPL_PAY_SUCCESS, {
first: { value: '支付成功通知' },
keyword1: { value: order.productName },
keyword2: { value: `¥${(order.price).toFixed(2)}` },
keyword3: { value: orderId },
keyword4: { value: order.createTime },
remark: { value: '感谢购买,点击查看订单详情' }
}, `https://example.com/order.html?id=${orderId}`)
}
}
res.send('<xml><return_code>SUCCESS</return_code></xml>')
})
module.exports = router
public/index.html - 商城首页
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微信商城</title>
</head>
<body>
<div id="app">
<div class="product-card">
<h3>精品课程</h3>
<p class="price">¥99.00</p>
<button onclick="buyNow('精品课程', 99)">立即购买</button>
</div>
</div>
<script>
async function buyNow(name, price) {
const res = await fetch('/order/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productName: name, price: price })
})
const { payParams } = await res.json()
WeixinJSBridge.invoke('getBrandWCPayRequest', payParams, function(res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
location.href = '/pay-success.html'
}
})
}
</script>
</body>
</html>