feat(mp, web, calendar): 新增微信收藏夹与日历批量生成功能
- 新增微信小程序版个人收藏夹全套功能,包含聚合首页、收藏列表、编辑、详情页面与配套后端API - 新增web版时光助手门户与个人收藏夹工具,支持与小程序共享后端数据 - 优化日历提醒工具:替换本地bootstrap资源为公共CDN版本,新增批量生成循环提醒的前端界面与后端逻辑,新增数据库batch_id字段,更新鉴权配置文件 - 补充项目配置、站点地图、调试诊断脚本等配套文件 - 完善各模块的页面配置与样式文件
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
Reference in New Issue
Block a user