Files
zhuomi-hui/server/lib/wechat.js
T
Michelle ZanandCursor 0ca2e87bf2 Ship signup sheet, subscribe reminders, cover crop, and theme updates.
Wire real WeChat app config, prefer activity cover over posters, and add admin cover cropping for the home banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-08 13:06:43 +08:00

101 lines
3.1 KiB
JavaScript

const crypto = require('crypto')
const { HttpError } = require('./http')
let tokenCache = null
function wxConfigured() {
return !!(process.env.WX_APPID && process.env.WX_SECRET)
}
async function code2session(code, deviceId) {
if (!wxConfigured()) {
const seed = deviceId || 'guest'
return {
openid: `demo_${crypto.createHash('sha1').update(seed).digest('hex').slice(0, 16)}`,
demo: true
}
}
const url = new URL('https://api.weixin.qq.com/sns/jscode2session')
url.search = new URLSearchParams({
appid: process.env.WX_APPID,
secret: process.env.WX_SECRET,
js_code: code,
grant_type: 'authorization_code'
}).toString()
let session
try {
const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
session = await res.json()
} catch (error) {
throw new HttpError(502, '微信登录服务不可用')
}
if (!session.openid) throw new HttpError(400, session.errmsg || '微信登录失败')
return { openid: session.openid, demo: false }
}
async function getAccessToken() {
if (!wxConfigured()) throw new HttpError(400, '未配置 WX_APPID / WX_SECRET,无法发送订阅消息')
if (tokenCache && tokenCache.expiresAt > Date.now() + 60_000) return tokenCache.token
const url = new URL('https://api.weixin.qq.com/cgi-bin/token')
url.search = new URLSearchParams({
grant_type: 'client_credential',
appid: process.env.WX_APPID,
secret: process.env.WX_SECRET
}).toString()
const res = await fetch(url, { signal: AbortSignal.timeout(8000) })
const body = await res.json()
if (!body.access_token) throw new HttpError(502, body.errmsg || '获取微信 access_token 失败')
tokenCache = {
token: body.access_token,
expiresAt: Date.now() + (Number(body.expires_in) || 7200) * 1000
}
return tokenCache.token
}
function clip(value, max) {
const text = value == null ? '' : String(value)
return text.length > max ? text.slice(0, max) : text
}
// Default field mapping for a typical “活动提醒” template (thing / time / thing).
function buildRemindData(activity) {
return {
thing1: { value: clip(activity.title || '卓米会活动', 20) },
time2: { value: clip(activity.startAt || '待定', 20) },
thing3: { value: clip(activity.address || '详见活动页', 20) }
}
}
async function sendSubscribeMessage({ openid, templateId, page, data }) {
const token = await getAccessToken()
const res = await fetch(
`https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=${encodeURIComponent(token)}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
touser: openid,
template_id: templateId,
page: page || '',
data,
miniprogram_state: process.env.WX_MINI_STATE || 'formal'
}),
signal: AbortSignal.timeout(8000)
}
)
const body = await res.json()
return { ok: body.errcode === 0, errcode: body.errcode, errmsg: body.errmsg || '' }
}
module.exports = {
wxConfigured,
code2session,
getAccessToken,
buildRemindData,
sendSubscribeMessage
}