feat: 新增作息报时器功能并替换短链接前缀为/to/
- 新增作息报时器独立模块,包含完整的日程管理、语音播报和待机功能 - 短链接正式前缀由/l/改为/to/,解决手机小屏下与I、i、1字形混淆问题 - 保留旧前缀/l/、/L/、/I/、/i/兼容已分发的旧短链 - 新增短码大小写兜底匹配,仅唯一匹配时生效避免歧义 - 新增防爆紧急替换功能,可轮换首页鉴权并替换短链接 - 新增多个调试和集成测试脚本,保障数据安全和功能回归
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* 作息报时器(schedule_reminder)后端接口集成测试
|
||||
* 独立起一个临时 express 实例(8978),只挂载 schedule_reminder 的路由,
|
||||
* 不依赖 8976 主服务,也不影响运行中的服务。
|
||||
*
|
||||
* 数据库隔离策略:脚本开始时若 data/app_order_box_schedule_reminder.db 不存在,
|
||||
* 结束时把测试期间产生的 db / wal / shm 一并删除,保证不留测试脏数据。
|
||||
*
|
||||
* 运行:node dev_test_scripts/integration/test_schedule_reminder_integration.js
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const express = require('express')
|
||||
|
||||
const DB_FILE = path.join(process.cwd(), 'data', 'app_order_box_schedule_reminder.db')
|
||||
const DB_EXISTED_BEFORE = fs.existsSync(DB_FILE)
|
||||
|
||||
const { bindRoutes } = require('../../src/server/app_order_box/kits/schedule_reminder')
|
||||
|
||||
const PORT = 8978
|
||||
const BASE = `http://127.0.0.1:${PORT}`
|
||||
|
||||
let passed = 0
|
||||
let failed = 0
|
||||
|
||||
const check = (name, ok, extra) => {
|
||||
if (ok) { passed += 1; console.log(` ✔ ${name}`) }
|
||||
else { failed += 1; console.log(` ✘ ${name}${extra ? ' → ' + extra : ''}`) }
|
||||
}
|
||||
|
||||
const request = async (method, url, body) => {
|
||||
const res = await fetch(BASE + url, {
|
||||
method,
|
||||
headers: body === undefined ? undefined : { 'Content-Type': 'application/json' },
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
})
|
||||
const text = await res.text()
|
||||
let json = null
|
||||
try { json = JSON.parse(text) } catch (_) { /* 非 JSON */ }
|
||||
return { status: res.status, json, text }
|
||||
}
|
||||
|
||||
const cleanupDb = () => {
|
||||
if (DB_EXISTED_BEFORE) return
|
||||
;['', '-wal', '-shm'].forEach(suffix => {
|
||||
const p = DB_FILE + suffix
|
||||
try { if (fs.existsSync(p)) fs.unlinkSync(p) } catch (_) { /* 静默 */ }
|
||||
})
|
||||
console.log('\n(测试库为本次新建,已清理:' + path.basename(DB_FILE) + ')')
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
console.log('=== 作息报时器 · 后端接口集成测试 ===\n')
|
||||
|
||||
// ---------- 1. 校时接口 ----------
|
||||
console.log('[1] GET /time 校时')
|
||||
const timeRes = await request('GET', '/api/app_order_box/kits/schedule_reminder/time')
|
||||
check('HTTP 200', timeRes.status === 200, timeRes.status)
|
||||
check('返回 ok=true', !!timeRes.json && timeRes.json.ok === true)
|
||||
const epochMs = timeRes.json && timeRes.json.data ? Number(timeRes.json.data.epochMs) : NaN
|
||||
check('epochMs 是有效时间戳', Number.isFinite(epochMs) && Math.abs(Date.now() - epochMs) < 60 * 1000, epochMs)
|
||||
|
||||
// ---------- 2. 日程校验 ----------
|
||||
console.log('\n[2] POST /schedules 参数校验')
|
||||
const noTitle = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { items: [{ time: '07:30', name: '起床' }] })
|
||||
check('缺 title → 400', noTitle.status === 400, noTitle.status)
|
||||
const noItem = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { title: '周末', weekdays: [6, 7] })
|
||||
check('缺 items → 400', noItem.status === 400, noItem.status)
|
||||
const noWeekday = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', { title: '周末', items: [{ time: '07:30', name: '起床' }] })
|
||||
check('缺 weekdays → 400', noWeekday.status === 400, noWeekday.status)
|
||||
|
||||
// ---------- 3. 新建日程 ----------
|
||||
console.log('\n[3] POST /schedules 新建(含清洗规则)')
|
||||
const createRes = await request('POST', '/api/app_order_box/kits/schedule_reminder/schedules', {
|
||||
title: ' 周末在家 ',
|
||||
weekdays: [7, 6, 6, 99, 0], // 期望清洗为 [6,7]
|
||||
items: [
|
||||
{ time: '11:00', name: '做午饭', repeatTimes: 5, intervalSec: 30 },
|
||||
{ time: '07:30', name: '起床', repeatTimes: 99, intervalSec: 1 }, // 期望被夹到 20 / 5
|
||||
{ time: '25:99', name: '非法时刻' }, // 期望被过滤
|
||||
],
|
||||
})
|
||||
check('HTTP 200', createRes.status === 200, createRes.status + ' ' + createRes.text.slice(0, 120))
|
||||
const created = createRes.json && createRes.json.data
|
||||
check('title 已 trim', created && created.title === '周末在家', created && created.title)
|
||||
check('weekdays 清洗为 [6,7]', created && JSON.stringify(created.weekdays) === '[6,7]', created && JSON.stringify(created.weekdays))
|
||||
check('非法时刻被过滤(剩 2 条)', created && created.items.length === 2, created && created.items.length)
|
||||
check('items 按时刻升序', created && created.items[0].time === '07:30' && created.items[1].time === '11:00',
|
||||
created && created.items.map(i => i.time).join(','))
|
||||
const first = created && created.items[0]
|
||||
check('repeatTimes 夹到 20', first && first.repeatTimes === 20, first && first.repeatTimes)
|
||||
check('intervalSec 夹到 5', first && first.intervalSec === 5, first && first.intervalSec)
|
||||
check('默认 repeatTimes=5 / intervalSec=30', created && created.items[1].repeatTimes === 5 && created.items[1].intervalSec === 30,
|
||||
created && (created.items[1].repeatTimes + '/' + created.items[1].intervalSec))
|
||||
const scheduleId = created && created.id
|
||||
|
||||
// ---------- 4. 列表 ----------
|
||||
console.log('\n[4] GET /schedules 列表')
|
||||
const listRes = await request('GET', '/api/app_order_box/kits/schedule_reminder/schedules')
|
||||
check('HTTP 200', listRes.status === 200, listRes.status)
|
||||
check('列表含新建日程', Array.isArray(listRes.json.data) && listRes.json.data.some(s => s.id === scheduleId))
|
||||
|
||||
// ---------- 5. 更新 ----------
|
||||
console.log('\n[5] PUT /schedules 更新')
|
||||
const updateRes = await request('PUT', '/api/app_order_box/kits/schedule_reminder/schedules', {
|
||||
id: scheduleId,
|
||||
title: '周末在家 v2',
|
||||
weekdays: [6, 7],
|
||||
items: [{ time: '07:30', name: '起床', repeatTimes: 5, intervalSec: 30 }],
|
||||
})
|
||||
check('HTTP 200', updateRes.status === 200, updateRes.status + ' ' + updateRes.text.slice(0, 120))
|
||||
check('title 已更新', updateRes.json && updateRes.json.data.title === '周末在家 v2')
|
||||
check('items 已更新为 1 条', updateRes.json && updateRes.json.data.items.length === 1)
|
||||
const missingUpdate = await request('PUT', '/api/app_order_box/kits/schedule_reminder/schedules', {
|
||||
id: 'not-exist-id', title: 'x', weekdays: [1], items: [{ time: '08:00', name: 'a' }],
|
||||
})
|
||||
check('更新不存在的日程 → 404', missingUpdate.status === 404, missingUpdate.status)
|
||||
|
||||
// ---------- 6. 播报日志 ----------
|
||||
console.log('\n[6] 播报日志 logs / logs/ack')
|
||||
const logRes = await request('POST', '/api/app_order_box/kits/schedule_reminder/logs', {
|
||||
scheduleId: scheduleId,
|
||||
scheduleTitle: '周末在家 v2',
|
||||
itemId: 'item-1',
|
||||
itemName: '起床',
|
||||
plannedAt: '2026-09-30T07:30:00+08:00',
|
||||
status: 'fired',
|
||||
})
|
||||
check('写日志 HTTP 200', logRes.status === 200, logRes.status + ' ' + logRes.text.slice(0, 120))
|
||||
const logId = logRes.json && logRes.json.data && logRes.json.data.id
|
||||
check('返回日志 id', Number.isFinite(Number(logId)) && Number(logId) > 0, logId)
|
||||
check('状态为 fired', logRes.json && logRes.json.data.status === 'fired')
|
||||
|
||||
const missingLog = await request('POST', '/api/app_order_box/kits/schedule_reminder/logs', { itemName: 'x' })
|
||||
check('缺 itemId → 400', missingLog.status === 400, missingLog.status)
|
||||
|
||||
const ackRes = await request('POST', '/api/app_order_box/kits/schedule_reminder/logs/ack', { id: logId })
|
||||
check('确认 HTTP 200', ackRes.status === 200, ackRes.status)
|
||||
check('状态变为 acked', ackRes.json && ackRes.json.data && ackRes.json.data.status === 'acked',
|
||||
ackRes.json && ackRes.json.data && ackRes.json.data.status)
|
||||
|
||||
const logsRes = await request('GET', '/api/app_order_box/kits/schedule_reminder/logs')
|
||||
check('日志列表 HTTP 200', logsRes.status === 200, logsRes.status)
|
||||
check('列表含刚写入的记录', Array.isArray(logsRes.json.data) && logsRes.json.data.some(l => Number(l.id) === Number(logId)))
|
||||
|
||||
// ---------- 7. 语音预合成参数校验 ----------
|
||||
console.log('\n[7] POST /tts/prepare 参数校验(不做真实合成)')
|
||||
const ttsBad = await request('POST', '/api/app_order_box/kits/schedule_reminder/tts/prepare', { schedule: {} })
|
||||
check('空 schedule → 400', ttsBad.status === 400, ttsBad.status)
|
||||
|
||||
// ---------- 8. 删除 ----------
|
||||
console.log('\n[8] DELETE /schedules 删除')
|
||||
const delRes = await request('DELETE', `/api/app_order_box/kits/schedule_reminder/schedules?id=${encodeURIComponent(scheduleId)}`)
|
||||
check('HTTP 200', delRes.status === 200, delRes.status)
|
||||
const afterDel = await request('GET', '/api/app_order_box/kits/schedule_reminder/schedules')
|
||||
check('删除后列表不再包含', Array.isArray(afterDel.json.data) && !afterDel.json.data.some(s => s.id === scheduleId))
|
||||
|
||||
console.log(`\n=== 结果:通过 ${passed} 项,失败 ${failed} 项 ===`)
|
||||
return failed === 0
|
||||
}
|
||||
|
||||
const app = express()
|
||||
app.use(express.json())
|
||||
bindRoutes(app)
|
||||
const server = app.listen(PORT, async () => {
|
||||
let ok = false
|
||||
try {
|
||||
ok = await run()
|
||||
} catch (e) {
|
||||
console.log('\n✘ 测试执行异常:' + String((e && e.stack) || e))
|
||||
failed += 1
|
||||
}
|
||||
server.close(() => {
|
||||
cleanupDb()
|
||||
process.exit(ok ? 0 : 1)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user