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:
Michelle Zan
2026-10-08 13:06:43 +08:00
co-authored by Cursor
parent cd05113755
commit 0ca2e87bf2
35 changed files with 968 additions and 184 deletions
+67 -1
View File
@@ -3,6 +3,7 @@ const express = require('express')
const QRCode = require('qrcode')
const db = require('../lib/db')
const passwords = require('../lib/passwords')
const wechat = require('../lib/wechat')
const { uploader } = require('../lib/upload')
const { adminAuth } = require('../lib/auth')
const { uid, token, ok, fail, wrap, str } = require('../lib/http')
@@ -162,6 +163,69 @@ router.delete('/activities/:id', auth, (req, res) => {
ok(res, { id: req.params.id })
})
router.get('/settings', auth, (req, res) => {
const settings = db.read().settings || {}
ok(res, {
subscribeTmplId: settings.subscribeTmplId || '',
wxConfigured: wechat.wxConfigured()
})
})
router.put('/settings', auth, (req, res) => {
const subscribeTmplId = str(req.body && req.body.subscribeTmplId, 80)
const settings = db.update((data) => {
if (!data.settings) data.settings = {}
data.settings.subscribeTmplId = subscribeTmplId
return data.settings
})
ok(res, settings)
})
router.post(
'/activities/:id/remind',
auth,
wrap(async (req, res) => {
const data = db.read()
const activity = data.activities.find((item) => item.id === req.params.id)
if (!activity) return fail(res, 404, '活动不存在')
const tmplId = str(data.settings && data.settings.subscribeTmplId)
if (!tmplId) return fail(res, 400, '请先在后台配置订阅消息模板 ID')
const targets = data.signups.filter((item) => item.activityId === activity.id && item.remindAccepted)
if (!targets.length) return fail(res, 400, '没有可提醒的报名用户(需用户报名时同意订阅)')
if (!wechat.wxConfigured()) {
return ok(res, {
demo: true,
total: targets.length,
sent: targets.length,
failed: 0,
message: '演示模式:未配置微信密钥,已模拟发送成功'
})
}
const page = `pages/detail/detail?id=${activity.id}`
const payload = wechat.buildRemindData(activity)
let sent = 0
const failures = []
for (const signup of targets) {
try {
const result = await wechat.sendSubscribeMessage({
openid: signup.openid,
templateId: tmplId,
page,
data: payload
})
if (result.ok) sent += 1
else failures.push({ openid: signup.openid, errmsg: result.errmsg || String(result.errcode) })
} catch (error) {
failures.push({ openid: signup.openid, errmsg: error.message || '发送失败' })
}
}
ok(res, { demo: false, total: targets.length, sent, failed: failures.length, failures: failures.slice(0, 20) })
})
)
router.get('/posters', auth, (req, res) => {
ok(res, db.read().posters)
})
@@ -227,7 +291,7 @@ function csvCell(value) {
}
router.get('/signups/export', adminAuth({ allowQueryToken: true }), (req, res) => {
const header = ['姓名', '手机号', '公司', '活动', '分享人', '报名时间', '是否签到', '签到时间']
const header = ['姓名', '手机号', '公司', '职位', '活动', '分享人', '订阅提醒', '报名时间', '是否签到', '签到时间']
const lines = [header.join(',')]
signupRows(str(req.query.activityId)).forEach((item) => {
lines.push(
@@ -235,8 +299,10 @@ router.get('/signups/export', adminAuth({ allowQueryToken: true }), (req, res) =
item.name,
item.phone,
item.company,
item.position || '',
item.activityTitle,
item.sourceSharer,
item.remindAccepted ? '是' : '否',
item.createdAt,
item.checkedIn ? '是' : '否',
item.checkedInAt