feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能
- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API - 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据 - 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件 - 补充项目配置、站点地图、调试诊断脚本等配套文件 - 完善各模块的页面配置与样式文件
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
const path = require('path');
|
||||
process.chdir(path.resolve(__dirname, '..', '..'));
|
||||
const fs = require('fs');
|
||||
const testDir = path.join(__dirname, '..', 'debug', 'calendar_batch_test');
|
||||
if (!fs.existsSync(testDir)) fs.mkdirSync(testDir, { recursive: true });
|
||||
const ts = Date.now();
|
||||
const testDB = path.join(testDir, `cal_test_${ts}.db`);
|
||||
const testWal = testDB + '-wal';
|
||||
const testShm = testDB + '-shm';
|
||||
const srcDB = path.join(process.cwd(), 'data', 'calendar_reminder_backups', 'calendar_reminder_20260831_122130.db');
|
||||
const srcWal = srcDB + '-wal';
|
||||
const srcShm = srcDB + '-shm';
|
||||
console.log('Copying source snapshot DB to:', testDB);
|
||||
fs.copyFileSync(srcDB, testDB);
|
||||
if (fs.existsSync(srcWal)) fs.copyFileSync(srcWal, testWal);
|
||||
if (fs.existsSync(srcShm)) fs.copyFileSync(srcShm, testShm);
|
||||
|
||||
// 把 calendar_reminder.js 的 DB_PATH 指向我们的临时 DB 副本
|
||||
// 做法:在 require calendar_reminder 前 hook better-sqlite3? 或者更简单:重写 require cache
|
||||
// 更简单:用进程内 NODE_OPTIONS 不行,但 require.cache 可以改。
|
||||
// 直接 patch module exports 之前先在 calendar_reminder 里用全局变量?
|
||||
// 最快做法:写一个 wrapper,覆盖 `const DB_PATH` 那行 -> 重新写 tempDBPath 赋值到 module scope 不方便,改用:
|
||||
// 把 `data/calendar_reminder.db` 临时 symlink/copy?但 Windows symlink 麻烦。
|
||||
// 就用最简单:创建独立 require 上下文。直接把 calendar_reminder.js 的 src 读出来、替换 DB_PATH,然后 vm.runInNewContext 太麻烦。
|
||||
// 更简便:我们用 require 的 module.__proto 拦截。
|
||||
|
||||
// 改文件替换(完成后改回):
|
||||
const modFile = path.join(process.cwd(), 'src', 'server', 'calendar_reminder.js');
|
||||
const orig = fs.readFileSync(modFile, 'utf8');
|
||||
const modded = orig.replace(
|
||||
/const DB_PATH = path\.join\(process\.cwd\(\), 'data', 'calendar_reminder\.db'\)/,
|
||||
`const DB_PATH = process.env.CALENDAR_DB_OVERRIDE || path.join(process.cwd(), 'data', 'calendar_reminder.db')`
|
||||
);
|
||||
if (modded === orig) { console.error('ERR: replace 未生效,DB_PATH 模式变了'); process.exit(3); }
|
||||
const backup = path.join(testDir, `calendar_reminder_${ts}.bak.js`);
|
||||
fs.copyFileSync(modFile, backup);
|
||||
fs.writeFileSync(modFile, modded, 'utf8');
|
||||
console.log('patched calendar_reminder.js to env override (backup at', backup, ')');
|
||||
|
||||
process.env.CALENDAR_DB_OVERRIDE = testDB;
|
||||
console.log('CALENDAR_DB_OVERRIDE =', process.env.CALENDAR_DB_OVERRIDE);
|
||||
|
||||
// 清 require cache
|
||||
for (const k of Object.keys(require.cache)) {
|
||||
if (k.includes('calendar_reminder') || k.includes('calendar_voice')) delete require.cache[k];
|
||||
}
|
||||
|
||||
const express = require('express');
|
||||
const Database = require('better-sqlite3');
|
||||
const mod = require(modFile);
|
||||
const db = new Database(testDB);
|
||||
|
||||
console.log('[自测] 确保 migrate 已执行:先 bindRoutes 一次');
|
||||
const express_check_app = express();
|
||||
mod.bindRoutes(express_check_app);
|
||||
const cols = new Set(db.prepare("PRAGMA table_info(events)").all().map(r => r.name));
|
||||
console.log('[自测] events 列(迁移后):', [...cols].join(','));
|
||||
if (!cols.has('batch_id')) { console.error('ERR: 迁移后仍然缺 batch_id'); process.exit(5) }
|
||||
const batchesExists = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='calendar_batches'").get();
|
||||
if (!batchesExists) { console.error('ERR: calendar_batches 未创建'); process.exit(6) }
|
||||
|
||||
const port = 8791;
|
||||
const app = express();
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use((req, res, next) => { req.headers.cookie = 'calendar_reminder_gate=1'; next(); });
|
||||
mod.bindRoutes(app);
|
||||
|
||||
const params = {
|
||||
batchName: '【自测】洗被子提醒 2026Q4-2027Q2',
|
||||
mode: 'monthlyByDay',
|
||||
rangeStart: '2026-09-01',
|
||||
rangeEnd: '2027-06-30',
|
||||
monthlyEvery: 2,
|
||||
monthlyDays: '1-5',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
remindTime: '08:30',
|
||||
allDay: false,
|
||||
completed: false,
|
||||
title: '洗被子提醒({MM}-{dd})',
|
||||
content: ''
|
||||
};
|
||||
|
||||
const server = app.listen(port, async () => {
|
||||
let exitCode = 0;
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const base = `http://127.0.0.1:${port}`;
|
||||
const headers = { 'Content-Type': 'application/json', 'Cookie': 'calendar_reminder_gate=1' };
|
||||
const ax = axios.create({ headers, validateStatus: () => true });
|
||||
|
||||
const r1 = await ax.post(`${base}/api/calendar_reminder/batch/preview`, params);
|
||||
console.log('[自测] 1.preview status=', r1.status, 'generated=', r1.data.generated, 'summary=', r1.data.summary);
|
||||
if (r1.status !== 200 || !r1.data.ok) { console.error('preview fail', r1.status, r1.data); throw new Error('preview') }
|
||||
if (r1.data.generated !== 25) { console.error('expected 25, got', r1.data.generated); throw new Error('count') }
|
||||
const batchId = r1.data.batchId;
|
||||
console.log('[自测] batch_id =', batchId);
|
||||
|
||||
const r2 = await ax.post(`${base}/api/calendar_reminder/batch/apply`, { batch_id: batchId, params: r1.data.params_saved, sql_full: r1.data.sql_full, skip_external_sync: true });
|
||||
console.log('[自测] 2.apply=', r2.status, r2.data);
|
||||
if (r2.status !== 200 || !r2.data.ok) throw new Error('apply');
|
||||
|
||||
const counts = db.prepare("SELECT deleted, COUNT(1) c FROM events WHERE batch_id=? GROUP BY deleted").all(batchId);
|
||||
console.log('[自测] 3.active counts=', counts);
|
||||
const active = (counts.find(r => r.deleted === 0) || {}).c || 0;
|
||||
if (active !== 25) throw new Error('active !=25, got ' + active);
|
||||
const batchRow = db.prepare("SELECT id, status, inserted_count, dedup_count, rollback_count, pre_existing_count, skip_external_sync FROM calendar_batches WHERE id=?").get(batchId);
|
||||
console.log('[自测] 4.batch row=', batchRow);
|
||||
|
||||
const rL = await ax.get(`${base}/api/calendar_reminder/batches?q=%E6%B4%97%E8%A2%AB%E5%AD%90`);
|
||||
console.log('[自测] 5.batches list len=', rL.data.data && rL.data.data.length);
|
||||
const rD = await ax.get(`${base}/api/calendar_reminder/batch/detail?id=${encodeURIComponent(batchId)}`);
|
||||
console.log('[自测] 6.detail counts=', rD.data.batch && rD.data.batch.counts, 'diag ok?', !!rD.data.diagnostics && !!rD.data.diagnostics.by_batch_id);
|
||||
const rE = await ax.get(`${base}/api/calendar_reminder/batch/export_sql?id=${encodeURIComponent(batchId)}`);
|
||||
console.log('[自测] 7.export_sql len=', typeof rE.data === 'string' ? rE.data.length : (Buffer.isBuffer(rE.data) ? rE.data.length : '??'), 'headers?', rE.headers && (rE.headers['content-disposition'] || rE.headers['content-type']));
|
||||
|
||||
const rR = await ax.post(`${base}/api/calendar_reminder/batch/rollback`, { id: batchId, hard: false });
|
||||
console.log('[自测] 8.rollback=', rR.status, rR.data);
|
||||
const c2 = db.prepare("SELECT deleted, COUNT(1) c FROM events WHERE batch_id=? GROUP BY deleted").all(batchId);
|
||||
console.log('[自测] 9.post-rollback counts=', c2);
|
||||
const d = (c2.find(r => r.deleted === 1) || {}).c || 0;
|
||||
if (d !== 25) throw new Error('deleted!=25, got ' + d);
|
||||
const rRepeat = await ax.post(`${base}/api/calendar_reminder/batch/preview`, Object.assign({}, params, { batchName: '【自测-复制】洗被子' }));
|
||||
console.log('[自测] 10.复制后的新批次 preview 预重(deleted=1 不命中去重)=', rRepeat.data.pre_existing_count);
|
||||
if (rRepeat.data.pre_existing_count !== 0) throw new Error('pre_existing_count !=0');
|
||||
|
||||
// 再做 apply 回滚然后 rollback 再做第二次去重测试
|
||||
const bid2 = rRepeat.data.batchId;
|
||||
const rA2 = await ax.post(`${base}/api/calendar_reminder/batch/apply`, { batch_id: bid2, params: rRepeat.data.params_saved, sql_full: rRepeat.data.sql_full, skip_external_sync: true });
|
||||
console.log('[自测] 11.复制 apply=', rA2.status, rA2.data);
|
||||
const rDup = await ax.post(`${base}/api/calendar_reminder/batch/preview`, Object.assign({}, params));
|
||||
console.log('[自测] 12.相同参数再预览(因为 bid2 刚 active=25,所以 pre_existing_count=25)=', rDup.data.pre_existing_count);
|
||||
if (rDup.data.pre_existing_count !== 25) throw new Error('pre_existing_count should be 25');
|
||||
|
||||
// 清理两条批次元数据 + events,让临时 DB 恢复干净(虽然是临时的,但也清一下)
|
||||
const del1 = db.prepare("DELETE FROM events WHERE batch_id IN (?,?)").run(batchId, bid2).changes;
|
||||
const del2 = db.prepare("DELETE FROM calendar_batches WHERE id IN (?,?)").run(batchId, bid2).changes;
|
||||
console.log('[自测] 13.cleanup del events=', del1, ', del batches=', del2);
|
||||
|
||||
// 再测 monthlyByNthWeekday:2026-09 到 2027-01 每月第 2 个周一,应得到 5 条
|
||||
const wd = { batchName: '房租 demo', mode: 'monthlyByNthWeekday', rangeStart: '2026-09-01', rangeEnd: '2027-01-31', monthlyEvery2: 1, nthWeek: 2, nthWeekday: 1, title: '交房租提醒 {date}', content: '', startTime: '09:00' };
|
||||
const rw = await ax.post(`${base}/api/calendar_reminder/batch/preview`, wd);
|
||||
console.log('[自测] 14.monthlyByNthWeekday=第2个周一 9月-1月 生成=', rw.data.generated, (rw.data.sample_list||[]).map(x=>x.start));
|
||||
if (rw.data.generated !== 5) throw new Error('monthlyByNthWeekday expected 5, got ' + rw.data.generated);
|
||||
|
||||
// weekly:每 2 周的周一、周三,2026-09-01~2026-10-15(每 2 周 2 条,6 周 * ?大概 6~7 条)
|
||||
const wk = { batchName: '健身', mode: 'weekly', rangeStart: '2026-09-01', rangeEnd: '2026-10-15', weeklyEvery: 2, weekdays: [1,3], title: '健身 {date} 周{W}', content: '', startTime: '20:00', endTime: '22:00', remindTime: '19:30' };
|
||||
const rwk = await ax.post(`${base}/api/calendar_reminder/batch/preview`, wk);
|
||||
console.log('[自测] 15.weekly 每 2 周 Mon/Wed 生成=', rwk.data.generated, 'list=', (rwk.data.sample_list||[]).map(x=>x.start + ' ' + x.title));
|
||||
|
||||
// daily:每 3 天 2026-09-01~2026-09-20 => 7条 (1 4 7 10 13 16 19)
|
||||
const dl = { batchName: '喝水', mode: 'daily', rangeStart: '2026-09-01', rangeEnd: '2026-09-20', dailyEvery: 3, title: '喝水打卡 {date}', startTime: '10:00' };
|
||||
const rdl = await ax.post(`${base}/api/calendar_reminder/batch/preview`, dl);
|
||||
console.log('[自测] 16.daily 每3天 9/1-20 生成=', rdl.data.generated, 'list=', (rdl.data.sample_list||[]).map(x=>x.start));
|
||||
if (rdl.data.generated !== 7) throw new Error('daily expected 7, got ' + rdl.data.generated);
|
||||
|
||||
console.log('\n✅ 自测全部通过。退出。');
|
||||
} catch (e) {
|
||||
console.error('[自测] 异常:', e.message, e.stack);
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
// 恢复 calendar_reminder.js 源码
|
||||
fs.copyFileSync(backup, modFile);
|
||||
fs.unlinkSync(backup);
|
||||
console.log('restored calendar_reminder.js 原文');
|
||||
// 删临时 DB
|
||||
try { fs.unlinkSync(testDB) } catch {}
|
||||
try { fs.unlinkSync(testWal) } catch {}
|
||||
try { fs.unlinkSync(testShm) } catch {}
|
||||
server.close(() => process.exit(exitCode));
|
||||
setTimeout(() => process.exit(exitCode), 2000).unref();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user