feat: 新增作息报时器功能并替换短链接前缀为/to/

- 新增作息报时器独立模块,包含完整的日程管理、语音播报和待机功能
- 短链接正式前缀由/l/改为/to/,解决手机小屏下与I、i、1字形混淆问题
- 保留旧前缀/l/、/L/、/I/、/i/兼容已分发的旧短链
- 新增短码大小写兜底匹配,仅唯一匹配时生效避免歧义
- 新增防爆紧急替换功能,可轮换首页鉴权并替换短链接
- 新增多个调试和集成测试脚本,保障数据安全和功能回归
This commit is contained in:
yangxiangyuan
2026-10-09 14:04:37 +08:00
parent ac475b6659
commit a3e8911deb
131 changed files with 7695 additions and 60 deletions
@@ -0,0 +1,89 @@
/**
* 数据体检工具:确认关键私有数据是否完好、有无测试残留
*
* 由来:2026-09-30 调试脚本曾误删用户真实日程;此后每次涉及数据的操作,
* 都跑一遍本工具确认「用户数据没少、测试垃圾没留」。只读,不修改任何数据。
*
* 运行:node dev_test_scripts/tools/tool_check_data_integrity.js
*/
const path = require('path')
const fs = require('fs')
const Database = require('better-sqlite3')
const dataDir = path.join(process.cwd(), 'data')
let problems = 0
const report = (label, value, ok = true) => {
if (!ok) problems += 1
console.log(` ${ok ? '✔' : '✘'} ${label}: ${value}`)
}
const openReadonly = name => {
const p = path.join(dataDir, name)
if (!fs.existsSync(p)) return null
return new Database(p, { readonly: true })
}
console.log('=== 数据体检(只读) ===\n')
// ---------- 1. 短链接 ----------
console.log('[1] 短链接 data/short_link.db')
const sdb = openReadonly('short_link.db')
if (!sdb) {
console.log(' (数据库不存在,跳过)')
} else {
const links = sdb.prepare('SELECT code, title, click_count, is_active FROM short_links ORDER BY id').all()
const clicks = sdb.prepare('SELECT COUNT(1) AS c FROM short_link_clicks').get().c
console.log(` 共 ${links.length} 条短链接:`)
links.forEach(r => console.log(` [${r.code}] ${r.title || '(无标题)'} clicks=${r.click_count} active=${r.is_active}`))
console.log(` 点击记录总数:${clicks}`)
// 测试残留短码:本工具测过的临时短码统一以 zz 开头
const stray = links.filter(r => String(r.code).startsWith('zz'))
report('测试残留短码(应为 0)', stray.length, stray.length === 0)
if (stray.length) stray.forEach(r => console.log(` → 残留 ${r.code}`))
// 点击记录里的孤立短码(短链已删但点击记录还在)
const codes = links.map(r => r.code)
const clickCodes = sdb.prepare('SELECT DISTINCT code FROM short_link_clicks').all().map(r => r.code)
const orphans = clickCodes.filter(c => codes.indexOf(c) < 0)
report('点击记录中的孤立短码(应为 0)', orphans.length, orphans.length === 0)
if (orphans.length) orphans.forEach(c => console.log(` → 孤立 ${c}`))
sdb.close()
}
// ---------- 2. 作息报时器 ----------
console.log('\n[2] 作息报时器 data/app_order_box_schedule_reminder.db')
const odb = openReadonly('app_order_box_schedule_reminder.db')
if (!odb) {
console.log(' (数据库不存在,跳过)')
} else {
const alive = odb.prepare('SELECT title, items_json FROM schedules WHERE deleted = 0').all()
const softDeleted = odb.prepare('SELECT COUNT(1) AS c FROM schedules WHERE deleted = 1').get().c
console.log(` 未删除的日程 ${alive.length} 套:`)
alive.forEach(r => {
let n = 0
try { n = JSON.parse(r.items_json || '[]').length } catch (_) {}
console.log(` ${r.title}(${n} 个时刻)`)
})
console.log(` 已软删除的历史行:${softDeleted}(这些是测试残留或用户主动删除,不影响使用)`)
const logs = odb.prepare('SELECT COUNT(1) AS c FROM logs').get().c
const testLogs = odb.prepare('SELECT COUNT(1) AS c FROM logs WHERE schedule_title != ?').get('周三在公司').c
console.log(` 播报记录:${logs} 条(非用户日程的:${testLogs})`)
report('用户日程仍存在(至少 1 套)', alive.length, alive.length >= 1)
odb.close()
}
// ---------- 3. 运动提醒宝(框子另一个 kit) ----------
console.log('\n[3] 运动提醒宝 data/app_order_box_motion_reminder.db')
const mdb = openReadonly('app_order_box_motion_reminder.db')
if (!mdb) {
console.log(' (数据库不存在,跳过)')
} else {
const cards = mdb.prepare('SELECT title FROM cards WHERE deleted = 0').all()
console.log(` 未删除的流程卡片 ${cards.length} 张:${cards.map(c => c.title).join(' / ') || '(无)'}`)
mdb.close()
}
console.log(`\n=== 体检完成:${problems === 0 ? '未发现异常' : '发现 ' + problems + ' 处异常'} ===`)
process.exit(problems === 0 ? 0 : 1)