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 }