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