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>
This commit is contained in:
+15
-1
@@ -33,7 +33,11 @@ const SEED = {
|
||||
],
|
||||
posters: [],
|
||||
signups: [],
|
||||
checkinCodes: []
|
||||
checkinCodes: [],
|
||||
settings: {
|
||||
// 微信公众平台 → 订阅消息 → 选用「活动提醒」类模板后填入模板 ID
|
||||
subscribeTmplId: process.env.WX_SUBSCRIBE_TMPL_ID || ''
|
||||
}
|
||||
}
|
||||
|
||||
const COLLECTIONS = ['users', 'admins', 'adminAllowlist', 'activities', 'posters', 'signups', 'checkinCodes']
|
||||
@@ -49,6 +53,16 @@ function normalize(data) {
|
||||
dirty = true
|
||||
}
|
||||
})
|
||||
if (!data.settings || typeof data.settings !== 'object') {
|
||||
data.settings = { subscribeTmplId: process.env.WX_SUBSCRIBE_TMPL_ID || '' }
|
||||
dirty = true
|
||||
} else if (typeof data.settings.subscribeTmplId !== 'string') {
|
||||
data.settings.subscribeTmplId = process.env.WX_SUBSCRIBE_TMPL_ID || ''
|
||||
dirty = true
|
||||
} else if (!data.settings.subscribeTmplId && process.env.WX_SUBSCRIBE_TMPL_ID) {
|
||||
data.settings.subscribeTmplId = process.env.WX_SUBSCRIBE_TMPL_ID
|
||||
dirty = true
|
||||
}
|
||||
|
||||
// The allowlist is the only source of truth for mini-program admins.
|
||||
data.users.forEach((user) => {
|
||||
|
||||
@@ -24,7 +24,8 @@ function publicActivity(activity, posters) {
|
||||
theme: activity.theme || 0,
|
||||
cover: activity.cover || '',
|
||||
video: activity.video || '',
|
||||
poster: (poster && poster.image) || activity.cover || '',
|
||||
// Activity cover wins over linked posters so admin “封面图片” updates show immediately.
|
||||
poster: activity.cover || (poster && poster.image) || '',
|
||||
createdAt: activity.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
+72
-6
@@ -1,10 +1,14 @@
|
||||
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) {
|
||||
const appid = process.env.WX_APPID
|
||||
const secret = process.env.WX_SECRET
|
||||
if (!appid || !secret) {
|
||||
if (!wxConfigured()) {
|
||||
const seed = deviceId || 'guest'
|
||||
return {
|
||||
openid: `demo_${crypto.createHash('sha1').update(seed).digest('hex').slice(0, 16)}`,
|
||||
@@ -14,8 +18,8 @@ async function code2session(code, deviceId) {
|
||||
|
||||
const url = new URL('https://api.weixin.qq.com/sns/jscode2session')
|
||||
url.search = new URLSearchParams({
|
||||
appid,
|
||||
secret,
|
||||
appid: process.env.WX_APPID,
|
||||
secret: process.env.WX_SECRET,
|
||||
js_code: code,
|
||||
grant_type: 'authorization_code'
|
||||
}).toString()
|
||||
@@ -31,4 +35,66 @@ async function code2session(code, deviceId) {
|
||||
return { openid: session.openid, demo: false }
|
||||
}
|
||||
|
||||
module.exports = { code2session }
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user