/** * 数据体检工具:确认关键私有数据是否完好、有无测试残留 * * 由来: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)