Files
SubsTracker/src/index.js
wangwangit dac8e7dc1d refactor(data): 订阅仓库改为 KV 多 Key + 自动迁移
把 v2 的单 Key subscriptions JSON 数组改造为:
  sub_index           = JSON 数组 [id1, id2, ...]
  sub:{id}            = 单订阅完整数据
  schema_version      = 'v3'
  migrate:{step}      = 'done' 标记
  migration_lock      = 60s TTL 锁
  subscriptions_v2_backup = 旧数据 7 天 TTL 备份

新增:
- src/data/subscriptions.repo.js:低层 KV 仓库(listIds/listAll/getById/save/saveMany/deleteById/replaceAll)
- src/data/migrate.js:迁移编排器,可累加 step;带内存缓存避免重复检查;
  幂等 + 锁保护
- 在 src/index.js 入口(fetch + scheduled)开头调用 ensureMigrations
- src/data/subscriptions.js 改造为调用新 repo,单条 CRUD 不再触碰整数组
- src/services/scheduler.js 自动续订写入改用 subRepo.saveMany

测试:
- tests/data/migrate.test.js 16 条用例覆盖 repo CRUD、迁移幂等、锁、损坏 JSON 兜底
- 共 58 条测试全绿;wrangler dry-run 401 KiB

老用户升级:第一次 fetch / scheduled 触发后透明完成迁移;旧数据 7 天可回滚。

Refs Task 3 of refactor/v3-product-grade plan.
2026-05-24 17:59:06 +08:00

79 lines
2.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @ts-check
/**
* Worker 入口v3
*
* - fetch handler处理 HTTP 请求;首先确保 KV 数据已迁移到 v3 schema
* - scheduled handler每小时触发一次到期检查cron 0 * * * * UTC
*
* v3 起 schema 迁移由 src/data/migrate.js 自动完成(首次访问透明触发,幂等可重跑)。
*
* 后续 Task 7 会把 fetch handler 整体迁到 Hono 应用,本文件届时大幅简化。
*
* 维护人v3 重构 (2026-05)
*/
import { handleApiRequest } from './api/router.js';
import { handleAdminRequest, handleLoginPage } from './api/admin.js';
import { handleDebug } from './api/debug.js';
import { checkExpiringSubscriptions } from './services/scheduler.js';
import { getUserFromRequest } from './api/handlers/auth.js';
import { ensureMigrations } from './data/migrate.js';
export default {
async fetch(request, env, ctx) {
// 透明迁移v3 schema 不到位时先迁移再处理请求
try {
await ensureMigrations(env);
} catch (err) {
console.error('[index] 迁移失败,回退继续处理请求(用户会看到旧数据):', err);
}
const url = new URL(request.url);
if (url.pathname === '/') {
const { user } = await getUserFromRequest(request, env);
if (user) {
return new Response('', {
status: 302,
headers: { Location: '/admin' }
});
}
return handleLoginPage();
} else if (url.pathname === '/debug') {
// 调试页必须登录后才能访问,避免泄露系统信息
const { user } = await getUserFromRequest(request, env);
if (!user) {
return new Response('未授权访问', {
status: 401,
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
}
return handleDebug(request, env);
} else if (url.pathname.startsWith('/api')) {
return handleApiRequest(request, env);
} else if (url.pathname.startsWith('/admin')) {
return handleAdminRequest(request, env);
} else {
return handleLoginPage();
}
},
async scheduled(event, env, ctx) {
// Cron 触发也要确保迁移完成(首次部署后用户可能还没访问过页面)
try {
await ensureMigrations(env);
} catch (err) {
console.error('[index] scheduled 迁移失败:', err);
}
console.log(
'[Workers] 定时任务触发',
'cron:',
event?.cron || '(unknown)',
'UTC:',
new Date().toISOString()
);
await checkExpiringSubscriptions(env);
}
};