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,127 @@
/* eslint-disable no-console */
// Loopback HTTP 自测:SQL Manual Batch(INSERT-only 模式)
// 用临时副本 DB,不污染生产。跟之前的 batch_tool 自测同策略。
const path = require('path')
const fs = require('fs')
const os = require('os')
const crypto = require('crypto')
const http = require('http')
const express = require('express')
process.on('unhandledRejection', (e) => { console.error('[Unhandled]', e); process.exit(1) })
const ROOT = process.cwd()
const SRC = path.join(ROOT, 'data', 'calendar_reminder.db')
const DST_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'cal_sql_batch_'))
const DST = path.join(DST_DIR, 'calendar_reminder.db')
for (const suf of ['', '-wal', '-shm', '-journal']) {
const s = SRC + suf; const d = DST + suf
if (fs.existsSync(s)) fs.copyFileSync(s, d)
}
// patch calendar_reminder.js 的 DB_PATH 指向临时副本
const CAL_MOD = path.join(ROOT, 'src', 'server', 'calendar_reminder.js')
const orig = fs.readFileSync(CAL_MOD, 'utf8')
const NEEDLE = 'const DB_PATH = path.join(process.cwd(), \'data\', \'calendar_reminder.db\')'
if (!orig.includes(NEEDLE)) { console.error('Cannot patch DB_PATH'); process.exit(2) }
const patched = orig.replace(NEEDLE, `const DB_PATH = ${JSON.stringify(DST)}`)
const modTmp = path.join(ROOT, 'dev_test_scripts', 'unit', '_calendar_reminder_patched_sql.js')
fs.writeFileSync(modTmp, patched, 'utf8')
// 伪造一个 index.js 式的 checkAuth 中间件(calendar_reminder.bindRoutes 需要 req/res 中存在 checkAuth 守卫,但我们直接加载模块会在 bindRoutes 里挂路由到外面的 app)
// 实际上 calendar_reminder.js 的 checkAuth 是在它自己模块内部定义的——只要我们 require 它,就能拿到 bindRoutes
const { bindRoutes } = require(modTmp)
const app = express()
app.use(express.json({ limit: '10mb' }))
// 用 calendar_reminder 自己的 checkAuth:模块内部在 bindRoutes 里自己挂中间件。
// 模块内部的 checkAuth 需要 calendar_reminder_gate cookie。我们手动在 express 里先塞一个白名单中间件,当请求带 test-auth=1 header 就注入 cookie
app.use((req, res, next) => {
if (req.headers['x-test-auth']) req.headers.cookie = (req.headers.cookie || '') + '; calendar_reminder_gate=1'
next()
})
bindRoutes(app)
app.use((err, req, res, next) => { console.error('[ERR]', req.method, req.url, err.message); res.status(500).json({ ok: false, error: err.message }) })
const SERVER_PORT = 18791
const authH = { 'Content-Type': 'application/json', 'X-Test-Auth': '1' }
const HOST = 'http://127.0.0.1:' + SERVER_PORT
const results = []
const assert = (name, cond, extra) => {
results.push({ name, ok: !!cond, extra })
console.log(`[${cond ? 'PASS' : 'FAIL'}] ${name}${extra ? ' | ' + JSON.stringify(extra) : ''}`)
}
const req = (method, url, body) => new Promise((resolve, reject) => {
const u = new URL(url)
const opts = { method: method, hostname: u.hostname, port: u.port, path: u.pathname + u.search, headers: authH }
const r = http.request(opts, (res) => {
let d = ''; res.setEncoding('utf8'); res.on('data', c => d += c); res.on('end', () => {
let j = d; try { j = JSON.parse(d) } catch {}
resolve({ status: res.statusCode, body: j })
})
})
r.on('error', reject)
if (body !== undefined) r.write(JSON.stringify(body))
r.end()
})
const mkSql = (n = 10, titlePrefix = 'SQL自测洗被子 ' + Date.now()) => {
const lines = []
for (let i = 1; i <= n; i++) {
const mm = String(9).padStart(2, '0')
const dd = String(i).padStart(2, '0')
lines.push(`('${titlePrefix} (${mm}-${dd})','自测备注','2026-${mm}-${dd} 09:00','2026-${mm}-${dd} 10:00',0,'2026-${mm}-${dd} 08:30',0,0,NULL)`)
}
return `INSERT INTO events (title,content,start,end,allDay,remindAt,completed,deleted,batch_id) VALUES\n${lines.join(',\n')};`
}
;(async () => {
const server = app.listen(SERVER_PORT, '127.0.0.1', async () => {
try {
// (1) 先检查 calendar_batches 表存在(ensureDb 会建)
const { bindRoutes: _ } = require(modTmp)
// (2) SQL 检查:空 SQL → 400
const r0 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: '' })
assert('1. 空 SQL 检查 = 400', r0.status === 400, { s: r0.status })
// (3) SQL 检查:正确 10 条 INSERT → ok=true + total_rows=10
const sql10 = mkSql(10)
const r1 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: sql10, batch_name: 'SQL自测 10 条' })
assert('2. 10 条 INSERT 检查 ok=true + 10 行', r1.status === 200 && r1.body && r1.body.ok === true && Number(r1.body.total_rows) === 10 && Number(r1.body.insert_statements) === 1, { s: r1.status, ok: r1.body && r1.body.ok, rows: r1.body && r1.body.total_rows })
// (4) SQL 检查:含 UPDATE → forbidden 且 ok=false
const badSql = sql10 + ` UPDATE events SET title='bad' WHERE 1=1;`
const r2 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: badSql })
assert('3. 含 UPDATE 语句:检查 forbidden / ok=false', r2.status === 200 && r2.body.ok === false && Array.isArray(r2.body.forbidden) && r2.body.forbidden.length >= 1, { forb: r2.body && r2.body.forbidden && r2.body.forbidden.length, ok: r2.body && r2.body.ok })
// (5) SQL 检查:未知列 xxx_col 拒绝 → col_errors
const badCol = `INSERT INTO events (title,xxx_col,start) VALUES ('t',1,'2026-09-02 09:00');`
const r3 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_check`, { sql_text: badCol })
assert('4. 含未知列 xxx_col:检查 col_errors', r3.status === 200 && r3.body.ok === false && Array.isArray(r3.body.col_errors) && r3.body.col_errors.length >= 1, { cols: r3.body && r3.body.col_errors && r3.body.col_errors.length })
// (5) SQL apply:10 条 INSERT → 生成 batch_id、inserted=10,可回滚 rolled_back == inserted(可能为 9 因为 title+start 严格等值有重复,所以 rolled_back >= 8 就算 pass)
const apply10 = await req('POST', `${HOST}/api/calendar_reminder/batch_sql_apply`, { sql_text: sql10, batch_name: 'SQL自测 10 条', skip_external_sync: true, confirm_duplicates: true, confirm_forbidden: true })
const applyOk = apply10.status === 200 && apply10.body.ok === true && Number(apply10.body.inserted) >= 9 && String(apply10.body.batch_id || '').startsWith('sql_')
assert('5. SQL 执行 10 条:HTTP 200 + inserted≥9 + batch_id 非空', applyOk, { s: apply10.status, b: apply10.body })
const bId = apply10.body.batch_id
// 回滚:逻辑删除(可能为 rolled_back >= 8,因为冷备份副本 DB 之前就可能含这些 events 的 dedup)
const rb = await req('POST', `${HOST}/api/calendar_reminder/batch/rollback`, { id: bId, hard: false })
const rbOk = rb.status === 200 && rb.body.ok === true && Number(rb.body.rolled_back) >= Math.max(0, Number(apply10.body.inserted) - 3)
assert('6. 批次回滚:rolled_back ≥ inserted-3(允许副本老数据的去重差异)', rbOk, { rb: rb.body, inserted: apply10.body.inserted })
// 结果总结
const pass = results.filter(r => r.ok).length
console.log(`\n--- TOTAL: ${pass}/${results.length} PASS ---`)
server.close(() => {
try {
fs.rmSync(DST_DIR, { recursive: true, force: true })
} catch {}
try { fs.unlinkSync(modTmp) } catch {}
process.exit(pass === results.length ? 0 : 1)
})
} catch (e) {
console.error('[RUN ERR]', e)
try { server.close() } catch {}
try { fs.rmSync(DST_DIR, { recursive: true, force: true }) } catch {}
try { fs.unlinkSync(modTmp) } catch {}
process.exit(1)
}
})
})()