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 }