Files
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

124 lines
3.4 KiB
JavaScript

const fs = require('fs')
const path = require('path')
const passwords = require('./passwords')
const file = process.env.DB_FILE
? path.resolve(process.env.DB_FILE)
: path.join(__dirname, '../data/db.json')
const SEED = {
users: [],
admins: [{ username: 'admin', password: '123456', token: '' }],
adminAllowlist: [],
activities: [
{
id: '93',
title: '2026年卓米第93期卓米会',
brandName: '卓米会 ZHUOMI',
brandDesc: '专注于品牌增长与企业服务的共创平台,连接资源、方法与同行者。',
cover: '',
video: '',
startAt: '2026-06-18 14:00',
address: '上海市浦东新区世纪大道 100 号 卓米中心 3F',
latitude: 31.235929,
longitude: 121.506321,
status: 'current',
isCurrent: true,
pinned: false,
pinnedAt: '',
theme: 0,
createdAt: '2026-06-01T00:00:00.000Z',
updatedAt: '2026-06-01T00:00:00.000Z'
}
],
posters: [],
signups: [],
checkinCodes: [],
settings: {
// 微信公众平台 → 订阅消息 → 选用「活动提醒」类模板后填入模板 ID
subscribeTmplId: process.env.WX_SUBSCRIBE_TMPL_ID || ''
}
}
const COLLECTIONS = ['users', 'admins', 'adminAllowlist', 'activities', 'posters', 'signups', 'checkinCodes']
let cache = null
// Returns true when the stored shape changed and must be persisted.
function normalize(data) {
let dirty = false
COLLECTIONS.forEach((key) => {
if (!Array.isArray(data[key])) {
data[key] = []
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) => {
// Client temp paths (http://tmp/..., wxfile://...) were saved by older builds and never resolve.
if (user.avatarUrl && !user.avatarUrl.startsWith('/static/uploads/')) {
user.avatarUrl = ''
dirty = true
}
if (!('role' in user)) return
if (user.role === 'admin' && !data.adminAllowlist.includes(user.openid)) {
data.adminAllowlist.push(user.openid)
}
delete user.role
dirty = true
})
data.admins.forEach((admin) => {
if (!passwords.isHashed(admin.password)) {
admin.password = passwords.hash(admin.password || '')
dirty = true
}
})
return dirty
}
function persist(data) {
fs.mkdirSync(path.dirname(file), { recursive: true })
const tmp = `${file}.${process.pid}.tmp`
fs.writeFileSync(tmp, JSON.stringify(data, null, 2))
fs.renameSync(tmp, file)
}
function load() {
if (cache) return cache
const data = fs.existsSync(file)
? JSON.parse(fs.readFileSync(file, 'utf8'))
: JSON.parse(JSON.stringify(SEED))
if (normalize(data) || !fs.existsSync(file)) persist(data)
cache = data
return cache
}
function read() {
return load()
}
function update(mutator) {
const data = load()
const result = mutator(data)
persist(data)
return result
}
function reset() {
cache = null
}
module.exports = { read, update, reset, file, SEED }