feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能

- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API
- 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据
- 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件
- 补充项目配置、站点地图、调试诊断脚本等配套文件
- 完善各模块的页面配置与样式文件
This commit is contained in:
yangxiangyuan
2026-09-02 22:20:37 +08:00
parent 132d132473
commit db7648eb4d
75 changed files with 8618 additions and 70 deletions
@@ -0,0 +1,31 @@
// 调试:确认 2026-04-11 批量插入的 47 条明细,以及是否属于"多肉养护-*"种子
const path = require('path');
const Database = require('better-sqlite3');
const DB_PATH = path.join(process.cwd(), 'data', 'calendar_reminder.db');
const db = new Database(DB_PATH);
const rows = db.prepare(`SELECT id, substr(id,1,20) AS id_pre, title, substr(start,1,16) AS start, substr(createdAt,1,16) AS created, deleted, completed, remindAt
FROM events WHERE substr(createdAt,1,10) = '2026-04-11' ORDER BY title, start`).all();
console.log(`2026-04-11 写入总数=${rows.length}`);
const byTitle = {};
rows.forEach(r => {
byTitle[r.title] = (byTitle[r.title] || 0) + 1;
});
console.log('\n按 title 聚合:');
Object.entries(byTitle).sort((a,b)=>b[1]-a[1]).forEach(([t,c]) => console.log(` x${c} ${t}`));
// 标题含"多肉养护-"的数量
const succu = rows.filter(r => r.title && r.title.includes('多肉养护-'));
console.log(`\ntitle 含 "多肉养护-" 的数量=${succu.length}`);
succu.slice(0,10).forEach(r => console.log(` ${r.title} | start=${r.start} | id=${r.id_pre} | del=${r.deleted}`));
// 看是否有重复种子(比如未来重复批次)
const dup = db.prepare(`SELECT title, COUNT(1) c FROM events
WHERE title LIKE '多肉养护-%'
GROUP BY title HAVING c > 1
ORDER BY c DESC`).all();
console.log('\n"多肉养护-*" 重复 title 汇总:');
dup.forEach(r => console.log(` x${r.c} ${r.title}`));
db.close();
@@ -0,0 +1,72 @@
// 调试:查询 calendar_reminder 数据库中"多肉"相关的批量插入记录统计
const path = require('path');
const fs = require('fs');
const Database = require('better-sqlite3');
const DB_PATH = path.join(process.cwd(), 'data', 'calendar_reminder.db');
console.log('DB exists:', fs.existsSync(DB_PATH), DB_PATH);
if (!fs.existsSync(DB_PATH)) { process.exit(1); }
const db = new Database(DB_PATH, { readonly: true });
db.pragma('journal_mode = WAL');
console.log('\n=== 表结构 ===');
db.prepare("PRAGMA table_info(events)").all().forEach(c => {
console.log(` ${c.name} (${c.type})`);
});
const total = db.prepare('SELECT COUNT(1) AS c FROM events').get().c;
console.log(`\n总记录数 events: ${total}`);
const byDel = db.prepare('SELECT deleted, COUNT(1) AS c FROM events GROUP BY deleted').all();
console.log('\n按 deleted 分组:');
byDel.forEach(r => console.log(` deleted=${r.deleted} -> ${r.c}`));
// 查询"多肉"相关(title / content 包含"多肉")
const kw = '%多肉%';
const succuAll = db.prepare("SELECT COUNT(1) AS c FROM events WHERE title LIKE ? OR content LIKE ?").get(kw, kw).c;
console.log(`\n含「多肉」的事件(title/content): ${succuAll}`);
if (succuAll > 0) {
const rows = db.prepare("SELECT id, title, substr(content,1,80) AS content, start, end, remindAt, recurrence, createdAt, updatedAt, deleted, completed FROM events WHERE title LIKE ? OR content LIKE ? ORDER BY createdAt DESC LIMIT 50").all(kw, kw);
console.log(`\n多肉记录抽样(最近 50 条):`);
rows.forEach(r => {
const tag = [
r.deleted ? '🗑删' : null,
r.completed ? '✅完' : null,
r.recurrence ? '🔁' + r.recurrence : null,
r.remindAt ? '⏰' : null
].filter(Boolean).join('');
console.log(` [${r.createdAt ? r.createdAt.slice(0,16) : '?'}] ${r.id.slice(0,8)} | ${r.title || '(无题)'} ${tag} | start=${r.start ? r.start.slice(0,16) : '?'} | content=${r.content || ''}`);
});
// 按 title 分组统计(看批量插入的规律)
const byTitle = db.prepare("SELECT title, COUNT(1) AS c, MIN(start) AS firstStart, MAX(start) AS lastStart FROM events WHERE title LIKE ? OR content LIKE ? GROUP BY title ORDER BY c DESC LIMIT 30").all(kw, kw);
console.log('\n按 title 聚合(多肉相关)TOP30:');
byTitle.forEach(r => console.log(` x${String(r.c).padStart(4,' ')} | ${r.firstStart ? r.firstStart.slice(0,10) : ''}~${r.lastStart ? r.lastStart.slice(0,10) : ''} | ${r.title || '(空)'}`));
// 按创建日期分组,看是否有批量插入特征
const byDay = db.prepare(`SELECT substr(createdAt,1,10) AS d, COUNT(1) AS c, COUNT(DISTINCT title) AS titles
FROM events WHERE title LIKE ? OR content LIKE ?
GROUP BY substr(createdAt,1,10) ORDER BY d DESC LIMIT 30`).all(kw, kw);
console.log('\n按 createdAt 日期聚合(多肉相关)TOP30:');
byDay.forEach(r => console.log(` ${r.d} x${String(r.c).padStart(4,' ')} distinct_titles=${r.titles}`));
// 按 recurrence 分组
const byRecur = db.prepare("SELECT recurrence, COUNT(1) AS c FROM events WHERE (title LIKE ? OR content LIKE ?) GROUP BY recurrence ORDER BY c DESC").all(kw, kw);
console.log('\n按 recurrence 分组:');
byRecur.forEach(r => console.log(` [${r.recurrence || '(无)'}] x${r.c}`));
// 删除 / 未删除 / 已完成比例
const stat = db.prepare(`SELECT
COUNT(1) AS total,
SUM(CASE WHEN deleted=1 THEN 1 ELSE 0 END) AS del,
SUM(CASE WHEN deleted=0 THEN 1 ELSE 0 END) AS alive,
SUM(CASE WHEN deleted=0 AND completed=1 THEN 1 ELSE 0 END) AS aliveDone,
SUM(CASE WHEN deleted=0 AND completed=0 THEN 1 ELSE 0 END) AS aliveTodo
FROM events WHERE title LIKE ? OR content LIKE ?`).get(kw, kw);
console.log('\n多肉记录状态:');
console.log(` 总数=${stat.total} 删除=${stat.del} 存活=${stat.alive} 存活中已完成=${stat.aliveDone} 存活中待办=${stat.aliveTodo}`);
}
db.close();
@@ -0,0 +1,64 @@
// 快速诊断:batch_sql_check 对含 xxx_col 的 INSERT 返回什么
// 用法:node 临时脚本放在项目根,直接 require
const fs = require('fs')
const path = require('path')
const os = require('os')
const TMP_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cal_diag_'))
const BACKUP_DIR = path.join(process.cwd(), 'data', 'calendar_reminder_backups')
const latest = fs.readdirSync(BACKUP_DIR).filter(f => f.startsWith('calendar_reminder_2') && f.endsWith('.db')).sort().slice(-1)[0]
const SRC = path.join(BACKUP_DIR, latest)
const DST_DIR = path.join(process.cwd(), 'data', 'calendar_reminder_backups', '_diag')
fs.mkdirSync(DST_DIR, { recursive: true })
for (const suf of ['.db', '.db-wal', '.db-shm']) {
fs.copyFileSync(SRC.replace(/\.db$/,'') + suf, path.join(DST_DIR, 'calendar_reminder' + suf))
}
const modSrc = fs.readFileSync(path.join(process.cwd(), 'src','server','calendar_reminder.js'), 'utf8')
const NEEDLE = "const DB_PATH = path.join(process.cwd(), 'data', 'calendar_reminder.db')"
if (!modSrc.includes(NEEDLE)) { throw new Error('NEEDLE NOT FOUND') }
const modTmp = path.join(process.cwd(), 'dev_test_scripts','unit','_calendar_reminder_diag.js')
fs.writeFileSync(modTmp, modSrc.replace(NEEDLE,
'const DB_PATH = path.join(process.cwd(), "data/calendar_reminder_backups/_diag", "calendar_reminder.db")'))
const express = require('express')
const { createHash } = require('crypto')
const cookie = require('cookie')
const app = express()
app.use((req, res, next) => {
req.headers.cookie = (req.headers.cookie || '') + '; calendar_reminder_gate=1'
next()
})
const mod = require(modTmp)
mod.bindRoutes(app)
const PORT = 18795
const server = app.listen(PORT, '127.0.0.1', async () => {
try {
const badCol = `INSERT INTO events (title,xxx_col,start) VALUES ('t',1,'2026-09-02 09:00');`
const http = require('http')
const r = await new Promise((resolve, reject) => {
const body = JSON.stringify({ sql_text: badCol })
const req = http.request({ hostname: '127.0.0.1', port: PORT, path: '/api/calendar_reminder/batch_sql_check', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } }, res => {
let buf = ''; res.on('data', d => buf += d); res.on('end', () => resolve({ status: res.statusCode, body: buf }))
})
req.on('error', reject); req.write(body); req.end()
})
console.log('STATUS', r.status)
console.log('BODY', r.body)
let j
try { j = JSON.parse(r.body) } catch (e) { j = null }
if (j) {
console.log('col_errors length:', j.col_errors && j.col_errors.length)
console.log('col_errors:', JSON.stringify(j.col_errors, null, 2))
console.log('syntax_errors:', JSON.stringify(j.syntax_errors, null, 2))
console.log('forbidden:', JSON.stringify(j.forbidden, null, 2))
console.log('ok?', j.ok)
}
server.close(() => {
fs.rmSync(DST_DIR, { recursive: true, force: true })
fs.unlinkSync(modTmp)
})
} catch (e) {
console.error(e)
try { server.close() } catch {}
try { fs.rmSync(DST_DIR, { recursive: true, force: true }) } catch {}
try { fs.unlinkSync(modTmp) } catch {}
}
})
@@ -0,0 +1,46 @@
// 只读诊断:用户刚执行的 16 条家人生日 SQL 是否真的入库
// 用法:node dev_test_scripts/debug/diag_birthday_sql.js
const path = require('path')
const fs = require('fs')
const Database = require('better-sqlite3')
const DB_PATH = path.join(process.cwd(), 'data', 'calendar_reminder.db')
if (!fs.existsSync(DB_PATH)) { console.error('DB NOT FOUND:', DB_PATH); process.exit(2) }
const db = new Database(DB_PATH, { readonly: true, fileMustExist: true })
console.log('=== 1. 最近 5 个 calendar_batches(找 sql_ 前缀最新批次) ===')
const batches = db.prepare(`SELECT id,title,status,params_json,generated_count,inserted_count,dedup_count,rollback_count,created_at,updated_at
FROM calendar_batches ORDER BY rowid DESC LIMIT 5`).all()
for (const b of batches) {
const p = b.params_json ? JSON.parse(b.params_json) : null
console.log(` - [${b.id}] ${b.title} | status=${b.status} | gen=${b.generated_count} ins=${b.inserted_count} dedup=${b.dedup_count} rb=${b.rollback_count} | created=${b.created_at} | range=${p && p.rangeStart}~${p && p.rangeEnd} | source=${p && p.source}`)
}
console.log('\n=== 2. 搜索 16 条 id 前缀的事件(bd_fa_ / bd_mo_ / bd_si_ / bd_me_) ===')
const prefixes = ['bd_fa_', 'bd_mo_', 'bd_si_', 'bd_me_']
const like = prefixes.map(p => `id LIKE '${p}%'`).join(' OR ')
const rows = db.prepare(`SELECT id,title,start,end,allDay,remindAt,deleted,completed,batch_id FROM events
WHERE ${like} ORDER BY start ASC`).all()
console.log(` 命中 ${rows.length} 行(期望 16)`)
for (const r of rows) {
console.log(` - [${r.id.slice(0,32)}...] ${r.title} | ${r.start} ~ ${r.end} | allDay=${r.allDay} remindAt=${r.remindAt} | deleted=${r.deleted} done=${r.completed} | batch=${r.batch_id}`)
}
if (rows.length === 16) {
const byY = {}
for (const r of rows) { const y = r.start.slice(0,4); byY[y] = (byY[y]||0)+1 }
console.log(' 按年分布:', JSON.stringify(byY))
const missingRemind = rows.filter(r => !r.remindAt)
console.log(' 未设置提醒(期望 0):', missingRemind.length)
const notAllDay = rows.filter(r => r.allDay != 1)
console.log(' 非全天事件(期望 0):', notAllDay.length)
const deletedOnes = rows.filter(r => r.deleted != 0)
console.log(' 已逻辑删除(期望 0):', deletedOnes.length)
const batch_ids = [...new Set(rows.filter(r => r.batch_id).map(r => r.batch_id))]
console.log(' 关联 batch_id 集合(期望 1 个 sql_ 前缀):', batch_ids)
}
console.log('\n=== 3. 时间跨度 sanity check(期望 start∈[2027-06-15, 2030-12-01]) ===')
const span = db.prepare(`SELECT MIN(start) min_s, MAX(end) max_e FROM events WHERE ${like}`).get()
console.log(' min start =', span.min_s, ' | max end =', span.max_e)